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

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


The following commit(s) were added to refs/heads/master by this push:
     new e8061edf668 Optimize load logic (#18153)
e8061edf668 is described below

commit e8061edf668cc88cf7f951edce97f5ca907d79b9
Author: Zhenyu Luo <[email protected]>
AuthorDate: Fri Jul 10 10:33:34 2026 +0800

    Optimize load logic (#18153)
    
    * Fix load write permission check
    
    * fix async
    
    * Optimize load auth tests
    
    * Optimize load user path encoding
    
    * fix async
    
    * Optimize load auth review fixes
    
    * fix async
---
 .../apache/iotdb/db/it/IoTDBLoadTsFileAuthIT.java  | 285 +++++++++++++++++++++
 .../iotdb/db/i18n/StorageEngineMessages.java       |   3 +
 .../iotdb/db/i18n/StorageEngineMessages.java       |   3 +
 .../protocol/thrift/IoTDBDataNodeReceiver.java     |   3 +-
 .../plan/analyze/load/LoadTsFileAnalyzer.java      |   5 +-
 .../load/TreeSchemaAutoCreatorAndVerifier.java     |  72 +++++-
 .../load/active/ActiveLoadPathHelper.java          |  50 +++-
 .../load/active/ActiveLoadTsFileLoader.java        |   9 +
 .../iotdb/db/storageengine/load/util/LoadUtil.java |  28 +-
 .../load/active/ActiveLoadPathHelperTest.java      | 146 +++++++++++
 10 files changed, 580 insertions(+), 24 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileAuthIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileAuthIT.java
new file mode 100644
index 00000000000..d842941e0cc
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileAuthIT.java
@@ -0,0 +1,285 @@
+/*
+ * 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.iotdb.db.it;
+
+import org.apache.iotdb.commons.auth.entity.PrivilegeType;
+import org.apache.iotdb.it.env.EnvFactory;
+import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.it.utils.TsFileGenerator;
+import org.apache.iotdb.itbase.category.ClusterIT;
+import org.apache.iotdb.itbase.category.LocalStandaloneIT;
+import org.apache.iotdb.jdbc.IoTDBSQLException;
+
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.external.commons.io.FileUtils;
+import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.write.schema.IMeasurementSchema;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.iotdb.db.it.utils.TestUtils.assertNonQueryTestFail;
+import static org.apache.iotdb.db.it.utils.TestUtils.createUser;
+import static org.apache.iotdb.db.it.utils.TestUtils.executeNonQuery;
+import static org.apache.iotdb.db.it.utils.TestUtils.grantUserSeriesPrivilege;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({LocalStandaloneIT.class, ClusterIT.class})
+public class IoTDBLoadTsFileAuthIT {
+  private static final long PARTITION_INTERVAL = 10 * 1000L;
+  private static final String DATABASE = "root.load_auth";
+  private static final String DEVICE = DATABASE + ".d1";
+  private static final IMeasurementSchema MEASUREMENT =
+      new MeasurementSchema("s1", TSDataType.INT32, TSEncoding.RLE);
+  private static final String NO_WRITE_USER = "load_no_write_user";
+  private static final String WRITE_USER = "load_write_user";
+  private static final String OTHER_PATH_WRITE_USER = 
"load_other_path_write_user";
+  private static final String ASYNC_NO_WRITE_USER = "async_load_no_write_user";
+  private static final String ASYNC_WRITE_USER = "async_load_write_user";
+  private static final String PASSWORD = "test123123456";
+  private static final long 
UNALLOCATABLE_TABLET_CONVERSION_BATCH_MEMORY_SIZE_IN_BYTES =
+      Long.MAX_VALUE / 4;
+
+  private static File tmpDir;
+
+  @BeforeClass
+  public static void setUp() throws Exception {
+    tmpDir = new File(Files.createTempDirectory("load-auth").toUri());
+    
EnvFactory.getEnv().getConfig().getCommonConfig().setTimePartitionInterval(PARTITION_INTERVAL);
+    
EnvFactory.getEnv().getConfig().getCommonConfig().setEnforceStrongPassword(false);
+    
EnvFactory.getEnv().getConfig().getCommonConfig().setAutoCreateSchemaEnabled(false);
+    EnvFactory.getEnv()
+        .getConfig()
+        .getDataNodeConfig()
+        .setMaxAllocateMemoryRatioForLoad(1.0)
+        .setLoadTsFileAnalyzeSchemaMemorySizeInBytes(10 * 1024L)
+        .setLoadTsFileTabletConversionBatchMemorySizeInBytes(
+            UNALLOCATABLE_TABLET_CONVERSION_BATCH_MEMORY_SIZE_IN_BYTES)
+        .setLoadActiveListeningCheckIntervalSeconds(1);
+
+    EnvFactory.getEnv().initClusterEnvironment();
+  }
+
+  @AfterClass
+  public static void tearDown() throws Exception {
+    deleteDatabase();
+    EnvFactory.getEnv().cleanClusterEnvironment();
+    FileUtils.deleteDirectory(tmpDir);
+  }
+
+  @After
+  public void cleanData() throws Exception {
+    deleteDatabase();
+  }
+
+  @Test
+  public void testLoadWithoutSchemaCheckStillChecksWriteDataPermission() 
throws Exception {
+    final File tsFile = new File(tmpDir, "1-0-0-0.tsfile");
+    prepareSchemaAndTsFile(tsFile);
+    createUser(NO_WRITE_USER, PASSWORD);
+
+    assertNonQueryTestFail(
+        String.format("load \"%s\" with ('database-level'='2', 
'verify'='false')", tsFile),
+        "No permissions for this operation, please add privilege WRITE_DATA",
+        NO_WRITE_USER,
+        PASSWORD);
+  }
+
+  @Test
+  public void testLoadWithoutSchemaCheckAllowsUserWithWriteDataPermission() 
throws Exception {
+    final File tsFile = new File(tmpDir, "2-0-0-0.tsfile");
+    prepareSchemaAndTsFile(tsFile);
+    createUser(WRITE_USER, PASSWORD);
+    grantUserSeriesPrivilege(WRITE_USER, PrivilegeType.WRITE_DATA, DATABASE + 
".**");
+
+    executeNonQuery(
+        String.format("load \"%s\" with ('database-level'='2', 
'verify'='false')", tsFile),
+        WRITE_USER,
+        PASSWORD);
+
+    try (final Connection connection = EnvFactory.getEnv().getConnection();
+        final Statement statement = connection.createStatement();
+        final ResultSet resultSet = statement.executeQuery("select count(s1) 
from " + DEVICE)) {
+      Assert.assertTrue(resultSet.next());
+      Assert.assertEquals(10, resultSet.getLong(1));
+    }
+  }
+
+  @Test
+  public void 
testLoadWithoutSchemaCheckRejectsUserWithOtherPathWriteDataPermission()
+      throws Exception {
+    final File tsFile = new File(tmpDir, "3-0-0-0.tsfile");
+    prepareSchemaAndTsFile(tsFile);
+    createUser(OTHER_PATH_WRITE_USER, PASSWORD);
+    grantUserSeriesPrivilege(OTHER_PATH_WRITE_USER, PrivilegeType.WRITE_DATA, 
"root.other.**");
+
+    assertNonQueryTestFail(
+        String.format("load \"%s\" with ('database-level'='2', 
'verify'='false')", tsFile),
+        "No permissions for this operation, please add privilege WRITE_DATA",
+        OTHER_PATH_WRITE_USER,
+        PASSWORD);
+  }
+
+  @Test
+  public void testAsyncLoadShouldCheckWriteDataPermissionWithStoredUser() 
throws Exception {
+    final File noWriteTsFile = new File(tmpDir, "4-0-0-0.tsfile");
+    final File writeTsFile = new File(tmpDir, "5-0-0-0.tsfile");
+    prepareSchemaAndTsFile(noWriteTsFile);
+    generateTsFile(writeTsFile);
+    createUser(ASYNC_NO_WRITE_USER, PASSWORD);
+    createUser(ASYNC_WRITE_USER, PASSWORD);
+    grantUserSeriesPrivilege(ASYNC_WRITE_USER, PrivilegeType.WRITE_DATA, 
DATABASE + ".**");
+
+    executeNonQuery(
+        String.format(
+            "load \"%s\" with ('database-level'='2', 'async'='true', 
'on-success'='none', "
+                + "'verify'='false')",
+            noWriteTsFile.getAbsolutePath()),
+        ASYNC_NO_WRITE_USER,
+        PASSWORD);
+    executeNonQuery(
+        String.format(
+            "load \"%s\" with ('database-level'='2', 'async'='true', 
'on-success'='none', "
+                + "'verify'='false')",
+            writeTsFile.getAbsolutePath()),
+        ASYNC_WRITE_USER,
+        PASSWORD);
+
+    waitUntilAllActiveLoadPendingDirsAreEmpty(TimeUnit.SECONDS.toMillis(60));
+    assertCountEventually(10, TimeUnit.SECONDS.toMillis(60));
+  }
+
+  private static void prepareSchemaAndTsFile(final File tsFile) throws 
Exception {
+    prepareSchema(MEASUREMENT.getType());
+    generateTsFile(tsFile);
+  }
+
+  private static void prepareSchema(final TSDataType dataType) throws 
Exception {
+    try (final Connection connection = EnvFactory.getEnv().getConnection();
+        final Statement statement = connection.createStatement()) {
+      statement.execute("create database " + DATABASE);
+      statement.execute(
+          String.format(
+              "create timeseries %s.%s %s", DEVICE, 
MEASUREMENT.getMeasurementName(), dataType));
+    }
+  }
+
+  private static void generateTsFile(final File tsFile) throws Exception {
+    try (final TsFileGenerator generator = new TsFileGenerator(tsFile)) {
+      generator.registerTimeseries(DEVICE, 
Collections.singletonList(MEASUREMENT));
+      generator.generateData(DEVICE, 10, PARTITION_INTERVAL / 10, false);
+    }
+  }
+
+  private static void deleteDatabase() throws Exception {
+    try (final Connection connection = EnvFactory.getEnv().getConnection();
+        final Statement statement = connection.createStatement()) {
+      statement.execute("delete database " + DATABASE);
+    } catch (final IoTDBSQLException ignored) {
+    }
+  }
+
+  private File getActiveLoadPendingDir(final DataNodeWrapper dataNodeWrapper) {
+    return new File(
+        dataNodeWrapper.getNodePath()
+            + File.separator
+            + "ext"
+            + File.separator
+            + "load"
+            + File.separator
+            + "pending");
+  }
+
+  private void waitUntilAllActiveLoadPendingDirsAreEmpty(final long timeoutMs)
+      throws InterruptedException {
+    final long deadline = System.currentTimeMillis() + timeoutMs;
+    while (System.currentTimeMillis() < deadline) {
+      boolean hasTsFile = false;
+      for (final DataNodeWrapper dataNodeWrapper : 
EnvFactory.getEnv().getDataNodeWrapperList()) {
+        if (containsTsFile(getActiveLoadPendingDir(dataNodeWrapper))) {
+          hasTsFile = true;
+          break;
+        }
+      }
+      if (!hasTsFile) {
+        return;
+      }
+      Thread.sleep(500L);
+    }
+    Assert.fail("Timed out waiting for active load pending dirs to become 
empty");
+  }
+
+  private void assertCountEventually(final long expected, final long 
timeoutMs) throws Exception {
+    final long deadline = System.currentTimeMillis() + timeoutMs;
+    AssertionError lastError = null;
+    while (System.currentTimeMillis() < deadline) {
+      try (final Connection connection = EnvFactory.getEnv().getConnection();
+          final Statement statement = connection.createStatement();
+          final ResultSet resultSet =
+              statement.executeQuery(
+                  "select count(" + MEASUREMENT.getMeasurementName() + ") from 
" + DEVICE)) {
+        Assert.assertTrue(resultSet.next());
+        Assert.assertEquals(expected, resultSet.getLong(1));
+        return;
+      } catch (final AssertionError e) {
+        lastError = e;
+      }
+      Thread.sleep(500L);
+    }
+    if (lastError != null) {
+      throw lastError;
+    }
+    Assert.fail("Timed out waiting for count " + expected);
+  }
+
+  private boolean containsTsFile(final File root) {
+    if (root == null || !root.exists()) {
+      return false;
+    }
+    if (root.isFile()) {
+      return root.getName().endsWith(".tsfile");
+    }
+
+    final File[] children = root.listFiles();
+    if (children == null) {
+      return false;
+    }
+    for (final File child : children) {
+      if (containsTsFile(child)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
index f8b6e50ed53..512879bfd3c 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
@@ -538,11 +538,14 @@ public final class StorageEngineMessages {
   public static final String FAILED_COUNT_ACTIVE_DIRS_FILE_NUMBER = "Failed to 
count active listening dirs file number.";
   public static final String ACTIVE_LOAD_METRIC_COLLECTOR_REGISTERED = "Active 
load metric collector periodical jobs registered";
   public static final String DATABASE_NAME_MUST_NOT_BE_EMPTY = "Database name 
must not be empty.";
+  public static final String USER_NAME_MUST_NOT_BE_EMPTY = "User name must not 
be empty.";
   public static final String ERROR_EXECUTING_ACTIVE_LOAD_JOB = "Error occurred 
when executing active load periodical job.";
   public static final String ACTIVE_LOAD_EXECUTOR_STARTED = "Active load 
periodical jobs executor is started successfully.";
   public static final String ACTIVE_LOAD_EXECUTOR_STOPPED = "Active load 
periodical jobs executor is stopped successfully.";
   public static final String ACTIVE_LOAD_TEMPORARILY_UNAVAILABLE =
       "Rejecting auto load tsfile {} (isGeneratedByPipe = {}) due to temporary 
unavailability, will retry later. Status: {}";
+  public static final String USER_IN_ACTIVE_LOAD_PATH_DOES_NOT_EXIST =
+      "The user in the active load path does not exist";
   public static final String ERROR_MOVING_FILE_TO_FAIL_DIR = "Error occurred 
during moving file {} to fail directory.";
   public static final String FAILED_COUNT_FILES_IN_FAIL_DIR = "Failed to count 
failed files in fail directory.";
 
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
index bffda3d5287..96d09c1c141 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
@@ -538,11 +538,14 @@ public final class StorageEngineMessages {
   public static final String FAILED_COUNT_ACTIVE_DIRS_FILE_NUMBER = "统计 Active 
监听目录文件数量失败。";
   public static final String ACTIVE_LOAD_METRIC_COLLECTOR_REGISTERED = "Active 
加载指标收集定期任务已注册";
   public static final String DATABASE_NAME_MUST_NOT_BE_EMPTY = "数据库名称不能为空。";
+  public static final String USER_NAME_MUST_NOT_BE_EMPTY = "用户名不能为空。";
   public static final String ERROR_EXECUTING_ACTIVE_LOAD_JOB = "执行 Active 
加载定期任务时发生错误。";
   public static final String ACTIVE_LOAD_EXECUTOR_STARTED = "Active 
加载定期任务执行器已成功启动。";
   public static final String ACTIVE_LOAD_EXECUTOR_STOPPED = "Active 
加载定期任务执行器已成功停止。";
   public static final String ACTIVE_LOAD_TEMPORARILY_UNAVAILABLE =
       "拒绝自动加载 TsFile {} (isGeneratedByPipe = {}),原因是系统暂时不可用,将稍后重试。状态: {}";
+  public static final String USER_IN_ACTIVE_LOAD_PATH_DOES_NOT_EXIST =
+      "Active 加载路径中的用户不存在";
   public static final String ERROR_MOVING_FILE_TO_FAIL_DIR = "将文件 {} 
移动到失败目录时发生错误。";
   public static final String FAILED_COUNT_FILES_IN_FAIL_DIR = 
"统计失败目录中的失败文件数量失败。";
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
index 3048ad13084..81879355b62 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/receiver/protocol/thrift/IoTDBDataNodeReceiver.java
@@ -656,7 +656,8 @@ public class IoTDBDataNodeReceiver extends 
IoTDBFileReceiver {
         shouldConvertDataTypeOnTypeMismatch,
         validateTsFile || shouldConvertDataTypeOnTypeMismatch,
         null,
-        shouldMarkAsPipeRequest);
+        shouldMarkAsPipeRequest,
+        AuthorityChecker.SUPER_USER);
   }
 
   private TSStatus loadTsFileSync(final String dataBaseName, final String 
fileAbsolutePath)
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
index 158f38875e1..48d39165840 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
@@ -299,7 +299,8 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
               isConvertOnTypeMismatch,
               isVerifySchema,
               tabletConversionThresholdBytes,
-              isGeneratedByPipe);
+              isGeneratedByPipe,
+              Objects.nonNull(context) ? context.getUsername() : null);
 
       if (LoadUtil.loadTsFileAsyncToActiveDir(tsFiles, activeLoadAttributes, 
isDeleteAfterLoad)) {
         analysis.setFinishQueryAfterAnalyze(true);
@@ -551,6 +552,8 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
 
       if (isAutoCreateSchemaOrVerifySchemaEnabled) {
         getOrCreateTreeSchemaVerifier().autoCreateAndVerify(reader, 
device2TimeseriesMetadata);
+      } else {
+        
getOrCreateTreeSchemaVerifier().checkWritePermission(device2TimeseriesMetadata);
       }
 
       // TODO: how to get the correct write point count when
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/TreeSchemaAutoCreatorAndVerifier.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/TreeSchemaAutoCreatorAndVerifier.java
index a5e8f2d1c3c..6d1704c0cfd 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/TreeSchemaAutoCreatorAndVerifier.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/TreeSchemaAutoCreatorAndVerifier.java
@@ -154,20 +154,7 @@ public class TreeSchemaAutoCreatorAndVerifier {
           // not a timeseries, skip
         } else {
           // check WRITE_DATA permission of timeseries
-          long startTime = System.nanoTime();
-          try {
-            UserEntity userEntity = 
loadTsFileAnalyzer.context.getSession().getUserEntity();
-            TSStatus status =
-                AuthorityChecker.getAccessControl()
-                    .checkFullPathWriteDataPermission(
-                        userEntity, device, 
timeseriesMetadata.getMeasurementId());
-            if (status.getCode() != 
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
-              throw new AuthException(
-                  TSStatusCode.representOf(status.getCode()), 
status.getMessage());
-            }
-          } finally {
-            
PerformanceOverviewMetrics.getInstance().recordAuthCost(System.nanoTime() - 
startTime);
-          }
+          checkWritePermission(device, timeseriesMetadata.getMeasurementId());
           final Pair<CompressionType, TSEncoding> compressionEncodingPair =
               
reader.readTimeseriesCompressionTypeAndEncoding(timeseriesMetadata);
           schemaCache.addTimeSeries(
@@ -191,6 +178,63 @@ public class TreeSchemaAutoCreatorAndVerifier {
     }
   }
 
+  public void checkWritePermission(
+      Map<IDeviceID, List<TimeseriesMetadata>> device2TimeseriesMetadataList) 
throws AuthException {
+    for (final Map.Entry<IDeviceID, List<TimeseriesMetadata>> entry :
+        device2TimeseriesMetadataList.entrySet()) {
+      final IDeviceID device = entry.getKey();
+
+      try {
+        if (schemaCache.isDeviceDeletedByMods(device)) {
+          continue;
+        }
+      } catch (IllegalPathException e) {
+        LOGGER.warn(
+            DataNodeQueryMessages
+                
.FAILED_TO_CHECK_IF_DEVICE_ARG_IS_DELETED_BY_MODS_WILL_SEE_IT_AS_NOT_DELETED,
+            device,
+            e);
+      }
+
+      for (final TimeseriesMetadata timeseriesMetadata : entry.getValue()) {
+        try {
+          if (schemaCache.isTimeSeriesDeletedByMods(device, 
timeseriesMetadata)) {
+            continue;
+          }
+        } catch (IllegalPathException e) {
+          if (!timeseriesMetadata.getMeasurementId().isEmpty()) {
+            LOGGER.warn(
+                DataNodeQueryMessages
+                    
.FAILED_TO_CHECK_IF_DEVICE_ARG_TIMESERIES_ARG_IS_DELETED_BY_MODS_WILL_SEE_IT_AS_NOT,
+                device,
+                timeseriesMetadata.getMeasurementId(),
+                e);
+          }
+        }
+
+        if (!TSDataType.VECTOR.equals(timeseriesMetadata.getTsDataType())) {
+          checkWritePermission(device, timeseriesMetadata.getMeasurementId());
+        }
+      }
+    }
+  }
+
+  private void checkWritePermission(final IDeviceID device, final String 
measurementId)
+      throws AuthException {
+    final long startTime = System.nanoTime();
+    try {
+      UserEntity userEntity = 
loadTsFileAnalyzer.context.getSession().getUserEntity();
+      TSStatus status =
+          AuthorityChecker.getAccessControl()
+              .checkFullPathWriteDataPermission(userEntity, device, 
measurementId);
+      if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+        throw new AuthException(TSStatusCode.representOf(status.getCode()), 
status.getMessage());
+      }
+    } finally {
+      
PerformanceOverviewMetrics.getInstance().recordAuthCost(System.nanoTime() - 
startTime);
+    }
+  }
+
   /**
    * This can only be invoked after all timeseries in the current tsfile have 
been processed.
    * Otherwise, the isAligned status may be wrong.
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPathHelper.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPathHelper.java
index 7b971c15a10..991d368d395 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPathHelper.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPathHelper.java
@@ -25,6 +25,8 @@ import org.apache.iotdb.db.i18n.StorageEngineMessages;
 import org.apache.iotdb.db.queryengine.plan.statement.crud.LoadTsFileStatement;
 import org.apache.iotdb.db.storageengine.load.config.LoadTsFileConfigurator;
 
+import com.google.common.io.BaseEncoding;
+
 import java.io.File;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
@@ -43,10 +45,15 @@ import java.util.Optional;
 public final class ActiveLoadPathHelper {
 
   private static final String SEGMENT_SEPARATOR = "-";
+  public static final String USER_KEY = "user";
+  // Keep a version in the user path segment so future encryption algorithms 
can be added safely.
+  private static final String USER_VALUE_MASK_PREFIX = "v1-";
+  private static final BaseEncoding USER_VALUE_ENCODING = 
BaseEncoding.base32().omitPadding();
 
   private static final List<String> KEY_ORDER =
       Collections.unmodifiableList(
           Arrays.asList(
+              USER_KEY,
               LoadTsFileConfigurator.DATABASE_NAME_KEY,
               LoadTsFileConfigurator.DATABASE_LEVEL_KEY,
               LoadTsFileConfigurator.CONVERT_ON_TYPE_MISMATCH_KEY,
@@ -65,8 +72,13 @@ public final class ActiveLoadPathHelper {
       final Boolean convertOnTypeMismatch,
       final Boolean verify,
       final Long tabletConversionThresholdBytes,
-      final Boolean pipeGenerated) {
+      final Boolean pipeGenerated,
+      final String userName) {
     final Map<String, String> attributes = new LinkedHashMap<>();
+    if (Objects.nonNull(userName) && !userName.isEmpty()) {
+      attributes.put(USER_KEY, userName);
+    }
+
     if (Objects.nonNull(databaseName) && !databaseName.isEmpty()) {
       attributes.put(LoadTsFileConfigurator.DATABASE_NAME_KEY, databaseName);
     }
@@ -208,7 +220,15 @@ public final class ActiveLoadPathHelper {
   }
 
   private static String formatSegment(final String key, final String value) {
-    return key + SEGMENT_SEPARATOR + encodeValue(value);
+    return key + SEGMENT_SEPARATOR + encodeValue(maskValueIfNecessary(key, 
value));
+  }
+
+  private static String maskValueIfNecessary(final String key, final String 
value) {
+    if (!USER_KEY.equals(key)) {
+      return value;
+    }
+    return USER_VALUE_MASK_PREFIX
+        + USER_VALUE_ENCODING.encode(value.getBytes(StandardCharsets.UTF_8));
   }
 
   private static String encodeValue(final String value) {
@@ -227,7 +247,11 @@ public final class ActiveLoadPathHelper {
     }
 
     final String encodedValue = dirName.substring(prefixLength);
-    final String decodedValue = decodeValue(encodedValue);
+    final String rawDecodedValue = decodeValue(encodedValue);
+    if (USER_KEY.equals(key) && 
!rawDecodedValue.startsWith(USER_VALUE_MASK_PREFIX)) {
+      return Optional.empty();
+    }
+    final String decodedValue = unmaskValueIfNecessary(key, rawDecodedValue);
     try {
       validateAttributeValue(key, decodedValue);
       return Optional.of(decodedValue);
@@ -255,6 +279,11 @@ public final class ActiveLoadPathHelper {
       case LoadTsFileConfigurator.VERIFY_KEY:
         LoadTsFileConfigurator.validateVerifyParam(value);
         break;
+      case USER_KEY:
+        if (value == null || value.isEmpty()) {
+          throw new 
SemanticException(StorageEngineMessages.USER_NAME_MUST_NOT_BE_EMPTY);
+        }
+        break;
       default:
         LoadTsFileConfigurator.validateParameters(key, value);
     }
@@ -279,4 +308,19 @@ public final class ActiveLoadPathHelper {
       return value;
     }
   }
+
+  private static String unmaskValueIfNecessary(final String key, final String 
value) {
+    if (!USER_KEY.equals(key) || !value.startsWith(USER_VALUE_MASK_PREFIX)) {
+      return value;
+    }
+    return decodeUserName(value.substring(USER_VALUE_MASK_PREFIX.length()), 
value);
+  }
+
+  private static String decodeUserName(final String encodedUserName, final 
String fallback) {
+    try {
+      return new String(USER_VALUE_ENCODING.decode(encodedUserName), 
StandardCharsets.UTF_8);
+    } catch (final IllegalArgumentException e) {
+      return fallback;
+    }
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
index 5e6cade3087..ff5222e5754 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
@@ -261,6 +261,15 @@ public class ActiveLoadTsFileLoader {
             : new File(entry.getPendingDir());
     final Map<String, String> attributes = 
ActiveLoadPathHelper.parseAttributes(tsFile, pendingDir);
     ActiveLoadPathHelper.applyAttributesToStatement(attributes, statement, 
isVerify);
+    final String userName =
+        attributes.getOrDefault(ActiveLoadPathHelper.USER_KEY, 
AuthorityChecker.SUPER_USER);
+    final Optional<Long> userId = AuthorityChecker.getUserId(userName);
+    if (!userId.isPresent()) {
+      return new TSStatus(TSStatusCode.USER_NOT_EXIST.getStatusCode())
+          
.setMessage(StorageEngineMessages.USER_IN_ACTIVE_LOAD_PATH_DOES_NOT_EXIST);
+    }
+    session.setUserId(userId.get());
+    session.setUsername(userName);
 
     final File parentFile;
     if (statement.getDatabase() == null && entry.isTableModel()) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/util/LoadUtil.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/util/LoadUtil.java
index cafb1b0e700..88c71fb1cef 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/util/LoadUtil.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/util/LoadUtil.java
@@ -23,8 +23,11 @@ import org.apache.iotdb.commons.disk.FolderManager;
 import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType;
 import org.apache.iotdb.commons.exception.DiskSpaceInsufficientException;
 import org.apache.iotdb.commons.utils.RetryUtils;
+import org.apache.iotdb.db.auth.AuthorityChecker;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import org.apache.iotdb.db.i18n.StorageEngineMessages;
+import org.apache.iotdb.db.protocol.session.IClientSession;
+import org.apache.iotdb.db.protocol.session.SessionManager;
 import 
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
 import 
org.apache.iotdb.db.storageengine.dataregion.modification.v1.ModificationFileV1;
 import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
@@ -37,7 +40,7 @@ import org.slf4j.LoggerFactory;
 import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
-import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -124,8 +127,7 @@ public class LoadUtil {
       LOGGER.warn(StorageEngineMessages.LOAD_ACTIVE_LISTENING_DIR_NOT_SET);
       return false;
     }
-    final Map<String, String> attributes =
-        Objects.nonNull(loadAttributes) ? loadAttributes : 
Collections.emptyMap();
+    final Map<String, String> attributes = 
appendCurrentUserIfAbsent(loadAttributes);
     final File targetDir = 
ActiveLoadPathHelper.resolveTargetDir(targetFilePath, attributes);
 
     loadTsFileAsyncToTargetDir(
@@ -138,6 +140,23 @@ public class LoadUtil {
     return true;
   }
 
+  private static Map<String, String> appendCurrentUserIfAbsent(
+      final Map<String, String> loadAttributes) {
+    final Map<String, String> attributes =
+        Objects.nonNull(loadAttributes)
+            ? new LinkedHashMap<>(loadAttributes)
+            : new LinkedHashMap<>();
+    if (!attributes.containsKey(ActiveLoadPathHelper.USER_KEY)) {
+      final IClientSession session = 
SessionManager.getInstance().getCurrSession();
+      attributes.put(
+          ActiveLoadPathHelper.USER_KEY,
+          session == null || session.getUsername() == null
+              ? AuthorityChecker.SUPER_USER
+              : session.getUsername());
+    }
+    return attributes;
+  }
+
   public static boolean loadFilesToActiveDir(
       final Map<String, String> loadAttributes,
       final List<String> files,
@@ -161,8 +180,7 @@ public class LoadUtil {
       LOGGER.warn(StorageEngineMessages.LOAD_ACTIVE_LISTENING_DIR_NOT_SET);
       return false;
     }
-    final Map<String, String> attributes =
-        Objects.nonNull(loadAttributes) ? loadAttributes : 
Collections.emptyMap();
+    final Map<String, String> attributes = 
appendCurrentUserIfAbsent(loadAttributes);
     final File targetDir = 
ActiveLoadPathHelper.resolveTargetDir(targetFilePath, attributes);
 
     for (final String file : files) {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPathHelperTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPathHelperTest.java
new file mode 100644
index 00000000000..77936a653c7
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPathHelperTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.iotdb.db.storageengine.load.active;
+
+import org.apache.iotdb.db.queryengine.plan.statement.crud.LoadTsFileStatement;
+import org.apache.iotdb.db.storageengine.load.config.LoadTsFileConfigurator;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.Map;
+
+public class ActiveLoadPathHelperTest {
+
+  @Test
+  public void testUserAttributeShouldBeMaskedInPathAndDecodedWhenParsing() 
throws Exception {
+    final String userName = "active_load_user";
+    final File pendingDir = 
Files.createTempDirectory("active-load-path").toFile();
+    try {
+      final File targetDir =
+          ActiveLoadPathHelper.resolveTargetDir(
+              pendingDir,
+              ActiveLoadPathHelper.buildAttributes(null, null, null, null, 
null, null, userName));
+      final File tsFile = new File(targetDir, "1-0-0-0.tsfile");
+
+      Assert.assertTrue(targetDir.getAbsolutePath().contains("user-v1-"));
+      Assert.assertFalse(targetDir.getAbsolutePath().contains(userName));
+      Assert.assertFalse(targetDir.getAbsolutePath().contains("b64%3A"));
+
+      final Map<String, String> attributes =
+          ActiveLoadPathHelper.parseAttributes(tsFile, pendingDir);
+      Assert.assertEquals(userName, 
attributes.get(ActiveLoadPathHelper.USER_KEY));
+    } finally {
+      deleteRecursively(pendingDir);
+    }
+  }
+
+  @Test
+  public void testRawUserAttributeShouldBeIgnored() throws Exception {
+    final File pendingDir = 
Files.createTempDirectory("active-load-path").toFile();
+    try {
+      final File tsFile = new File(new File(pendingDir, 
"user-active_load_user"), "1-0-0-0.tsfile");
+
+      final Map<String, String> attributes =
+          ActiveLoadPathHelper.parseAttributes(tsFile, pendingDir);
+      
Assert.assertFalse(attributes.containsKey(ActiveLoadPathHelper.USER_KEY));
+    } finally {
+      deleteRecursively(pendingDir);
+    }
+  }
+
+  @Test
+  public void testNonV1UserAttributeShouldBeIgnored() throws Exception {
+    final File pendingDir = 
Files.createTempDirectory("active-load-path").toFile();
+    try {
+      final File tsFile =
+          new File(new File(pendingDir, "user-v2-active_load_user"), 
"1-0-0-0.tsfile");
+
+      final Map<String, String> attributes =
+          ActiveLoadPathHelper.parseAttributes(tsFile, pendingDir);
+      
Assert.assertFalse(attributes.containsKey(ActiveLoadPathHelper.USER_KEY));
+    } finally {
+      deleteRecursively(pendingDir);
+    }
+  }
+
+  @Test
+  public void 
testUnknownRawAttributeDirectoryShouldBeIgnoredForDowngradeCompatibility()
+      throws Exception {
+    final File pendingDir = 
Files.createTempDirectory("active-load-path").toFile();
+    try {
+      final File tsFile =
+          new File(new File(pendingDir, "future-load-param-future-value"), 
"1-0-0-0.tsfile");
+      createFile(tsFile);
+
+      final Map<String, String> attributes =
+          ActiveLoadPathHelper.parseAttributes(tsFile, pendingDir);
+      Assert.assertFalse(attributes.containsKey("future-load-param"));
+
+      final LoadTsFileStatement statement =
+          LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath());
+      ActiveLoadPathHelper.applyAttributesToStatement(attributes, statement, 
true);
+      Assert.assertTrue(statement.isVerifySchema());
+    } finally {
+      deleteRecursively(pendingDir);
+    }
+  }
+
+  @Test
+  public void 
testKnownPrefixWithInvalidFutureLikeValueShouldBeIgnoredForDowngradeCompatibility()
+      throws Exception {
+    final File pendingDir = 
Files.createTempDirectory("active-load-path").toFile();
+    try {
+      final File tsFile = new File(new File(pendingDir, 
"verify-future-value"), "1-0-0-0.tsfile");
+      createFile(tsFile);
+
+      final Map<String, String> attributes =
+          ActiveLoadPathHelper.parseAttributes(tsFile, pendingDir);
+      
Assert.assertFalse(attributes.containsKey(LoadTsFileConfigurator.VERIFY_KEY));
+
+      final LoadTsFileStatement statement =
+          LoadTsFileStatement.createUnchecked(tsFile.getAbsolutePath());
+      ActiveLoadPathHelper.applyAttributesToStatement(attributes, statement, 
true);
+      Assert.assertTrue(statement.isVerifySchema());
+    } finally {
+      deleteRecursively(pendingDir);
+    }
+  }
+
+  private static void deleteRecursively(final File file) {
+    if (file == null || !file.exists()) {
+      return;
+    }
+    final File[] children = file.listFiles();
+    if (children != null) {
+      for (final File child : children) {
+        deleteRecursively(child);
+      }
+    }
+    Assert.assertTrue(file.delete());
+  }
+
+  private static void createFile(final File file) throws Exception {
+    Assert.assertTrue(file.getParentFile().mkdirs());
+    Assert.assertTrue(file.createNewFile());
+  }
+}


Reply via email to