This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/dev/1.3 by this push:
new 5a58b8ba427 [to dev/1.3] Optimize load logic (#18153) (#18459)
5a58b8ba427 is described below
commit 5a58b8ba4272d618dd44f8876ef21af378f5f657
Author: Zhenyu Luo <[email protected]>
AuthorDate: Thu Aug 13 13:58:16 2026 +0800
[to dev/1.3] Optimize load logic (#18153) (#18459)
* 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
* fix(load): adapt load auth IT to branch tsfile/IT APIs
---
.../apache/iotdb/db/it/IoTDBLoadTsFileAuthIT.java | 283 +++++++++++++++++++++
.../protocol/thrift/IoTDBDataNodeReceiver.java | 3 +-
.../plan/analyze/load/LoadTsFileAnalyzer.java | 104 ++++++--
.../load/active/ActiveLoadPathHelper.java | 55 +++-
.../load/active/ActiveLoadTsFileLoader.java | 6 +-
.../storageengine/load/active/ActiveLoadUtil.java | 28 +-
.../load/active/ActiveLoadPathHelperTest.java | 103 +++++++-
7 files changed, 540 insertions(+), 42 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..09773e4533f
--- /dev/null
+++
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileAuthIT.java
@@ -0,0 +1,283 @@
+/*
+ * 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.commons.io.FileUtils;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.enums.TSEncoding;
+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 MeasurementSchema 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().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.getMeasurementId(), 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.getMeasurementId() + ") 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/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 92ff5ca2de5..b7da3775787 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
@@ -571,7 +571,8 @@ public class IoTDBDataNodeReceiver extends
IoTDBFileReceiver {
validateTsFile || shouldConvertDataTypeOnTypeMismatch ||
shouldWaitForSchemaBeforeLoad,
!shouldWaitForSchemaBeforeLoad,
null,
- shouldMarkAsPipeRequest);
+ shouldMarkAsPipeRequest,
+ AuthorityChecker.SUPER_USER);
}
private TSStatus loadTsFileSync(
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 227064b286a..a4a23478612 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
@@ -271,7 +271,8 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
isVerifySchema,
isAutoCreateSchemaRequested,
tabletConversionThresholdBytes,
- isGeneratedByPipe);
+ isGeneratedByPipe,
+ context.getSession().getUserName());
if (ActiveLoadUtil.loadTsFileAsyncToActiveDir(
tsFiles, activeLoadAttributes, isDeleteAfterLoad)) {
analysis.setFinishQueryAfterAnalyze(true);
@@ -465,6 +466,8 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
if (isAutoCreateSchemaOrVerifySchemaEnabled) {
schemaAutoCreatorAndVerifier.autoCreateAndVerify(reader,
device2TimeseriesMetadata);
+ } else {
+
schemaAutoCreatorAndVerifier.checkWritePermission(device2TimeseriesMetadata);
}
// TODO: how to get the correct write point count when
// !isAutoCreateSchemaOrVerifySchemaEnabled
@@ -673,33 +676,7 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
// not a timeseries, skip
} else {
// check WRITE_DATA permission of timeseries
- long startTime = System.nanoTime();
- try {
- String userName = context.getSession().getUserName();
- if (!AuthorityChecker.SUPER_USER.equals(userName)) {
- TSStatus status;
- try {
- List<PartialPath> paths =
- Collections.singletonList(
- new PartialPath(device,
timeseriesMetadata.getMeasurementId()));
- status =
- AuthorityChecker.getTSStatus(
- AuthorityChecker.checkFullPathListPermission(
- userName, paths,
PrivilegeType.WRITE_DATA.ordinal()),
- paths,
- PrivilegeType.WRITE_DATA);
- } catch (IllegalPathException e) {
- throw new RuntimeException(e);
- }
- 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(
@@ -723,6 +700,77 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
}
}
+ 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(
+ "Failed to check if device {} 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) {
+ // In aligned devices, there may be empty measurements which will
cause
+ // IllegalPathException.
+ if (!timeseriesMetadata.getMeasurementId().isEmpty()) {
+ LOGGER.warn(
+ "Failed to check if device {}, timeSeries {} is deleted by
mods. Will see it as not deleted.",
+ 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 {
+ String userName = context.getSession().getUserName();
+ if (!AuthorityChecker.SUPER_USER.equals(userName)) {
+ TSStatus status;
+ try {
+ List<PartialPath> paths =
+ Collections.singletonList(new PartialPath(device,
measurementId));
+ status =
+ AuthorityChecker.getTSStatus(
+ AuthorityChecker.checkFullPathListPermission(
+ userName, paths, PrivilegeType.WRITE_DATA.ordinal()),
+ paths,
+ PrivilegeType.WRITE_DATA);
+ } catch (IllegalPathException e) {
+ throw new RuntimeException(e);
+ }
+ 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 c9e33ffb5c2..7c131a2e912 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
@@ -24,6 +24,8 @@ import org.apache.iotdb.db.exception.sql.SemanticException;
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;
@@ -42,10 +44,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,
@@ -64,7 +71,8 @@ public final class ActiveLoadPathHelper {
final Boolean verify,
final Boolean autoCreateSchema,
final Long tabletConversionThresholdBytes,
- final Boolean pipeGenerated) {
+ final Boolean pipeGenerated,
+ final String userName) {
return buildAttributes(
null,
databaseLevel,
@@ -72,7 +80,8 @@ public final class ActiveLoadPathHelper {
verify,
autoCreateSchema,
tabletConversionThresholdBytes,
- pipeGenerated);
+ pipeGenerated,
+ userName);
}
public static Map<String, String> buildAttributes(
@@ -82,8 +91,12 @@ public final class ActiveLoadPathHelper {
final Boolean verify,
final Boolean autoCreateSchema,
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);
@@ -223,7 +236,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) {
@@ -242,7 +263,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);
@@ -273,6 +298,11 @@ public final class ActiveLoadPathHelper {
case LoadTsFileConfigurator.AUTO_CREATE_SCHEMA_KEY:
LoadTsFileConfigurator.validateAutoCreateSchemaParam(value);
break;
+ case USER_KEY:
+ if (value == null || value.isEmpty()) {
+ throw new SemanticException("User name must not be empty.");
+ }
+ break;
default:
LoadTsFileConfigurator.validateParameters(key, value);
}
@@ -298,4 +328,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 fddd8223903..7d661a18109 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
@@ -32,7 +32,6 @@ import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.protocol.session.IClientSession;
import org.apache.iotdb.db.protocol.session.InternalClientSession;
import org.apache.iotdb.db.protocol.session.SessionManager;
-import org.apache.iotdb.db.queryengine.common.SessionInfo;
import org.apache.iotdb.db.queryengine.plan.Coordinator;
import org.apache.iotdb.db.queryengine.plan.analyze.ClusterPartitionFetcher;
import
org.apache.iotdb.db.queryengine.plan.analyze.schema.ClusterSchemaFetcher;
@@ -264,6 +263,9 @@ 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);
+ session.setUsername(userName);
return executeStatement(
entry.isGeneratedByPipe() ? new PipeEnrichedStatement(statement) :
statement, session);
@@ -276,7 +278,7 @@ public class ActiveLoadTsFileLoader {
.executeForTreeModel(
statement,
SessionManager.getInstance().requestQueryId(),
- new SessionInfo(0, AuthorityChecker.SUPER_USER,
ZoneId.systemDefault(), ""),
+ SESSION_MANAGER.getSessionInfo(session),
"",
ClusterPartitionFetcher.getInstance(),
ClusterSchemaFetcher.getInstance(),
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadUtil.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadUtil.java
index a27bc6882df..93f8f04b481 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadUtil.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadUtil.java
@@ -21,8 +21,11 @@ package org.apache.iotdb.db.storageengine.load.active;
import org.apache.iotdb.commons.utils.FileUtils;
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.exception.DiskSpaceInsufficientException;
+import org.apache.iotdb.db.protocol.session.IClientSession;
+import org.apache.iotdb.db.protocol.session.SessionManager;
import org.apache.iotdb.db.storageengine.load.disk.ILoadDiskSelector;
import org.apache.iotdb.db.storageengine.rescon.disk.FolderManager;
import
org.apache.iotdb.db.storageengine.rescon.disk.strategy.DirectoryStrategyType;
@@ -37,8 +40,8 @@ import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.Comparator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -92,8 +95,7 @@ public class ActiveLoadUtil {
LOGGER.warn("Load active listening dir is 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);
transferFilesToActiveDir(
@@ -106,6 +108,23 @@ public class ActiveLoadUtil {
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,
@@ -129,8 +148,7 @@ public class ActiveLoadUtil {
LOGGER.warn("Load active listening dir is 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);
final List<File> sourceFiles = new ArrayList<>(files.size());
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
index 1a75dcce142..b0bab25eb90 100644
---
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
@@ -36,7 +36,7 @@ public class ActiveLoadPathHelperTest {
final File pendingDir =
Files.createTempDirectory("active-load-schema").toFile();
try {
final Map<String, String> attributes =
- ActiveLoadPathHelper.buildAttributes(null, null, null, true, false,
null, true);
+ ActiveLoadPathHelper.buildAttributes(null, null, null, true, false,
null, true, null);
final File targetDir = ActiveLoadPathHelper.resolveTargetDir(pendingDir,
attributes);
Assert.assertTrue(targetDir.mkdirs());
final File tsFile = new File(targetDir, "1-0-0-0.tsfile");
@@ -58,6 +58,102 @@ 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, 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;
@@ -70,4 +166,9 @@ public class ActiveLoadPathHelperTest {
}
Assert.assertTrue(file.delete());
}
+
+ private static void createFile(final File file) throws Exception {
+ Assert.assertTrue(file.getParentFile().mkdirs());
+ Assert.assertTrue(file.createNewFile());
+ }
}