This is an automated email from the ASF dual-hosted git repository.
HTHou 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 a8497bef9ff Record audit log when updating user password (#18357)
a8497bef9ff is described below
commit a8497bef9ffa0e1c2ac1177f29dac9f21370ecf7
Author: libo <[email protected]>
AuthorDate: Thu Jul 30 10:20:43 2026 +0800
Record audit log when updating user password (#18357)
---
.../iotdb/db/i18n/DataNodeQueryMessages.java | 2 +
.../iotdb/db/i18n/DataNodeQueryMessages.java | 2 +
.../org/apache/iotdb/db/audit/DNAuditLogger.java | 132 ++++++++-
.../iotdb/db/audit/PasswordChangeAuditContext.java | 187 ++++++++++++
.../iotdb/db/audit/PasswordChangeAuditTask.java | 87 ++++++
.../db/queryengine/common/MPPQueryContext.java | 6 +-
.../execution/config/TableConfigTaskVisitor.java | 36 ++-
.../execution/config/TreeConfigTaskVisitor.java | 30 +-
.../relational/security/AccessControlImpl.java | 3 +
.../security/TreeAccessCheckVisitor.java | 68 +++--
.../apache/iotdb/db/audit/DNAuditLoggerTest.java | 181 ++++++++++++
.../db/audit/PasswordChangeAuditContextTest.java | 327 +++++++++++++++++++++
12 files changed, 1018 insertions(+), 43 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index 4cb29ccfbc8..e8e1954751b 100644
---
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -3799,5 +3799,7 @@ public final class DataNodeQueryMessages {
public static final String EXCEPTION_ALIAS_IS_NULL_862D23B1 = "alias is
null";
public static final String EXCEPTION_REFERENCES_IS_NULL_25A7E3AF =
"references is null";
public static final String EXCEPTION_VISIBLEALIASES_IS_NULL_630B27F1 =
"visibleAliases is null";
+ public static final String
EXCEPTION_HAS_NO_PERMISSION_TO_EXECUTE_ARG_BECAUSE_ONLY_THE_SUPERUSER_CAN_ALTER_HIM_HERSELF_C5902893
=
+ "Has no permission to execute %s, because only the superuser can alter
him/herself.";
}
diff --git
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index 849ce165efe..c06b5332f7b 100644
---
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -4556,5 +4556,7 @@ public final class DataNodeQueryMessages {
public static final String EXCEPTION_ALIAS_IS_NULL_862D23B1 = "alias 不能为空";
public static final String EXCEPTION_REFERENCES_IS_NULL_25A7E3AF =
"references 不能为空";
public static final String EXCEPTION_VISIBLEALIASES_IS_NULL_630B27F1 =
"visibleAliases 不能为空";
+ public static final String
EXCEPTION_HAS_NO_PERMISSION_TO_EXECUTE_ARG_BECAUSE_ONLY_THE_SUPERUSER_CAN_ALTER_HIM_HERSELF_C5902893
=
+ "无权执行 %s,因为只有超级用户可以修改其自身。";
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/DNAuditLogger.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/DNAuditLogger.java
index 90026e37ba0..1f2b54d6d34 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/DNAuditLogger.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/DNAuditLogger.java
@@ -20,19 +20,41 @@
package org.apache.iotdb.db.audit;
import org.apache.iotdb.commons.audit.AbstractAuditLogger;
+import org.apache.iotdb.commons.audit.AuditEventType;
import org.apache.iotdb.commons.audit.AuditLogFields;
+import org.apache.iotdb.commons.audit.AuditLogOperation;
import org.apache.iotdb.commons.audit.IAuditEntity;
+import org.apache.iotdb.commons.audit.PrivilegeLevel;
+import org.apache.iotdb.commons.auth.entity.PrivilegeType;
import org.apache.iotdb.commons.exception.IllegalPathException;
import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.utils.CommonDateTimeUtils;
import org.apache.iotdb.db.queryengine.plan.Coordinator;
import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
+import org.apache.tsfile.common.conf.TSFileConfig;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.utils.Binary;
+
import jakarta.validation.constraints.NotNull;
+import java.util.Arrays;
import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
public class DNAuditLogger extends AbstractAuditLogger {
+ // This text matcher is only a fallback. Password-update semantic nodes must
clear sql_string
+ // before any audit entry is generated.
+ private static final Pattern ALTER_USER_PASSWORD_PATTERN =
+ Pattern.compile(
+ "^(\\s*ALTER\\s+USER\\s+.+?\\s+SET\\s+PASSWORD\\s+)"
+ + "(?:(?:U&)?'(?:''|[^'])*'|\"(?:\"\"|[^\"])*\")"
+ + "(\\s*;?\\s*)$",
+ Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
+ private static final Pattern VALUES_PATTERN =
Pattern.compile("(?i)(values)\\([^)]*\\)");
+
private Coordinator coordinator;
private DNAuditLogger() {
@@ -50,7 +72,115 @@ public class DNAuditLogger extends AbstractAuditLogger {
@NotNull
private static InsertRowStatement generateInsertStatement(
IAuditEntity auditLogFields, String log, PartialPath logDevice) {
- return null;
+ return generateInsertStatement(
+ auditLogFields, log, logDevice, CommonDateTimeUtils.currentTime());
+ }
+
+ @NotNull
+ static InsertRowStatement generateInsertStatement(
+ IAuditEntity auditLogFields, String log, PartialPath logDevice, long
logTimestamp) {
+ String username = auditLogFields.getUsername();
+ String address = auditLogFields.getCliHostname();
+ AuditEventType type = auditLogFields.getAuditEventType();
+ AuditLogOperation operation = auditLogFields.getAuditLogOperation();
+ PrivilegeLevel privilegeLevel = PrivilegeLevel.GLOBAL;
+ if (auditLogFields.getPrivilegeTypes() != null) {
+ for (PrivilegeType privilegeType : auditLogFields.getPrivilegeTypes()) {
+ privilegeLevel = judgePrivilegeLevel(privilegeType);
+ if (privilegeLevel == PrivilegeLevel.GLOBAL) {
+ break;
+ }
+ }
+ }
+
+ InsertRowStatement insertStatement = new InsertRowStatement();
+ insertStatement.setDevicePath(logDevice);
+ insertStatement.setTime(logTimestamp);
+ insertStatement.setMeasurements(
+ new String[] {
+ AUDIT_LOG_USERNAME,
+ AUDIT_LOG_CLI_HOSTNAME,
+ AUDIT_LOG_AUDIT_EVENT_TYPE,
+ AUDIT_LOG_OPERATION_TYPE,
+ AUDIT_LOG_PRIVILEGE_TYPE,
+ AUDIT_LOG_PRIVILEGE_LEVEL,
+ AUDIT_LOG_RESULT,
+ AUDIT_LOG_DATABASE,
+ AUDIT_LOG_SQL_STRING,
+ AUDIT_LOG_LOG
+ });
+ insertStatement.setAligned(true);
+ String sqlString = sanitizeAuditSql(auditLogFields.getSqlString());
+ insertStatement.setValues(
+ new Object[] {
+ new Binary(username == null ? "null" : username,
TSFileConfig.STRING_CHARSET),
+ new Binary(address == null ? "null" : address,
TSFileConfig.STRING_CHARSET),
+ new Binary(type == null ? "null" : type.toString(),
TSFileConfig.STRING_CHARSET),
+ new Binary(
+ operation == null ? "null" : operation.toString(),
TSFileConfig.STRING_CHARSET),
+ new Binary(
+ auditLogFields.getPrivilegeTypes() == null
+ ? "null"
+ : auditLogFields.getPrivilegeTypeString(),
+ TSFileConfig.STRING_CHARSET),
+ new Binary(privilegeLevel.toString(), TSFileConfig.STRING_CHARSET),
+ auditLogFields.getResult(),
+ new Binary(
+ auditLogFields.getDatabase() == null ? "null" :
auditLogFields.getDatabase(),
+ TSFileConfig.STRING_CHARSET),
+ new Binary(sqlString == null ? "null" : sqlString,
TSFileConfig.STRING_CHARSET),
+ new Binary(log == null ? "null" : log, TSFileConfig.STRING_CHARSET)
+ });
+ insertStatement.setDataTypes(
+ new TSDataType[] {
+ TSDataType.STRING,
+ TSDataType.STRING,
+ TSDataType.STRING,
+ TSDataType.STRING,
+ TSDataType.STRING,
+ TSDataType.STRING,
+ TSDataType.BOOLEAN,
+ TSDataType.STRING,
+ TSDataType.STRING,
+ TSDataType.STRING,
+ });
+ return insertStatement;
+ }
+
+ static String sanitizeAuditSql(String sqlString) {
+ if (sqlString == null) {
+ return null;
+ }
+ if (sqlString.regionMatches(true, 0, "CREATE USER", 0, "CREATE
USER".length())) {
+ sqlString = String.join(" ", Arrays.asList(sqlString.split("
")).subList(0, 3)) + " ...";
+ }
+ Matcher alterUserMatcher = ALTER_USER_PASSWORD_PATTERN.matcher(sqlString);
+ if (alterUserMatcher.matches()) {
+ sqlString = alterUserMatcher.replaceFirst("$1...$2");
+ }
+ return VALUES_PATTERN.matcher(sqlString).replaceAll("$1(...)");
+ }
+
+ private static PrivilegeLevel judgePrivilegeLevel(PrivilegeType type) {
+ if (type == null) {
+ return PrivilegeLevel.GLOBAL;
+ }
+ switch (type) {
+ case READ_DATA:
+ case DROP:
+ case ALTER:
+ case CREATE:
+ case DELETE:
+ case INSERT:
+ case SELECT:
+ case MANAGE_DATABASE:
+ case WRITE_DATA:
+ case READ_SCHEMA:
+ case WRITE_SCHEMA:
+ return PrivilegeLevel.OBJECT;
+ default:
+ return PrivilegeLevel.GLOBAL;
+ }
}
public void createViewIfNecessary() {}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditContext.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditContext.java
new file mode 100644
index 00000000000..d25064bf9cb
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditContext.java
@@ -0,0 +1,187 @@
+/*
+ * 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.audit;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.audit.AuditEventType;
+import org.apache.iotdb.commons.audit.AuditLogFields;
+import org.apache.iotdb.commons.audit.AuditLogOperation;
+import org.apache.iotdb.commons.audit.IAuditEntity;
+import org.apache.iotdb.commons.auth.entity.PrivilegeType;
+import org.apache.iotdb.commons.queryengine.common.SessionInfo;
+import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RelationalAuthorStatement;
+import org.apache.iotdb.db.queryengine.plan.relational.type.AuthorRType;
+import org.apache.iotdb.db.queryengine.plan.statement.AuthorType;
+import org.apache.iotdb.db.queryengine.plan.statement.Statement;
+import org.apache.iotdb.db.queryengine.plan.statement.sys.AuthorStatement;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+
+/** Holds the security-safe audit fields for one password-change statement. */
+public final class PasswordChangeAuditContext {
+
+ private static final PasswordChangeAuditContext EMPTY =
+ new PasswordChangeAuditContext(null, -1, null, null, null, null);
+
+ private final AuditLogWriter auditLogWriter;
+ private final long userId;
+ private final String username;
+ private final String clientHostname;
+ private final String database;
+ private final String targetUsername;
+ private final AtomicBoolean logged = new AtomicBoolean(false);
+
+ private PasswordChangeAuditContext(
+ AuditLogWriter auditLogWriter,
+ long userId,
+ String username,
+ String clientHostname,
+ String database,
+ String targetUsername) {
+ this.auditLogWriter = auditLogWriter;
+ this.userId = userId;
+ this.username = username;
+ this.clientHostname = clientHostname;
+ this.database = database;
+ this.targetUsername = targetUsername;
+ }
+
+ public static PasswordChangeAuditContext forTreeStatement(
+ Statement statement, SessionInfo sessionInfo) {
+ String targetUsername = getTreeTargetUsername(statement);
+ return targetUsername == null
+ ? EMPTY
+ : fromSessionInfo(sessionInfo, targetUsername,
DNAuditLogger.getInstance()::log);
+ }
+
+ static PasswordChangeAuditContext forTreeStatement(
+ Statement statement, SessionInfo sessionInfo, AuditLogWriter
auditLogWriter) {
+ String targetUsername = getTreeTargetUsername(statement);
+ return targetUsername == null
+ ? EMPTY
+ : fromSessionInfo(sessionInfo, targetUsername, auditLogWriter);
+ }
+
+ public static PasswordChangeAuditContext forTableStatement(
+ org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement
statement,
+ SessionInfo sessionInfo) {
+ String targetUsername = getTableTargetUsername(statement);
+ return targetUsername == null
+ ? EMPTY
+ : fromSessionInfo(sessionInfo, targetUsername,
DNAuditLogger.getInstance()::log);
+ }
+
+ static PasswordChangeAuditContext forTableStatement(
+ org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement
statement,
+ SessionInfo sessionInfo,
+ AuditLogWriter auditLogWriter) {
+ String targetUsername = getTableTargetUsername(statement);
+ return targetUsername == null
+ ? EMPTY
+ : fromSessionInfo(sessionInfo, targetUsername, auditLogWriter);
+ }
+
+ public static PasswordChangeAuditContext forTreeAuthorization(
+ Statement statement, IAuditEntity auditEntity) {
+ String targetUsername = getTreeTargetUsername(statement);
+ return targetUsername == null
+ ? EMPTY
+ : fromAuditEntity(auditEntity, targetUsername,
DNAuditLogger.getInstance()::log);
+ }
+
+ static PasswordChangeAuditContext forTreeAuthorization(
+ Statement statement, IAuditEntity auditEntity, AuditLogWriter
auditLogWriter) {
+ String targetUsername = getTreeTargetUsername(statement);
+ return targetUsername == null
+ ? EMPTY
+ : fromAuditEntity(auditEntity, targetUsername, auditLogWriter);
+ }
+
+ private static String getTreeTargetUsername(Statement statement) {
+ return statement instanceof AuthorStatement
+ && ((AuthorStatement) statement).getAuthorType() ==
AuthorType.UPDATE_USER
+ ? ((AuthorStatement) statement).getUserName()
+ : null;
+ }
+
+ private static String getTableTargetUsername(
+ org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement
statement) {
+ return statement instanceof RelationalAuthorStatement
+ && ((RelationalAuthorStatement) statement).getAuthorType() ==
AuthorRType.UPDATE_USER
+ ? ((RelationalAuthorStatement) statement).getUserName()
+ : null;
+ }
+
+ private static PasswordChangeAuditContext fromAuditEntity(
+ IAuditEntity auditEntity, String targetUsername, AuditLogWriter
auditLogWriter) {
+ return new PasswordChangeAuditContext(
+ auditLogWriter,
+ auditEntity.getUserId(),
+ auditEntity.getUsername(),
+ auditEntity.getCliHostname(),
+ auditEntity.getDatabase(),
+ targetUsername);
+ }
+
+ private static PasswordChangeAuditContext fromSessionInfo(
+ SessionInfo sessionInfo, String targetUsername, AuditLogWriter
auditLogWriter) {
+ return new PasswordChangeAuditContext(
+ auditLogWriter,
+ sessionInfo.getUserId(),
+ sessionInfo.getUserName(),
+ sessionInfo.getCliHostname(),
+ sessionInfo.getDatabaseName().orElse(null),
+ targetUsername);
+ }
+
+ public void log(TSStatus status) {
+ if (targetUsername == null || !logged.compareAndSet(false, true)) {
+ return;
+ }
+ boolean result =
+ status != null
+ && (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()
+ || status.getCode() ==
TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode());
+ auditLogWriter.log(
+ new AuditLogFields(
+ userId,
+ username,
+ clientHostname,
+ AuditEventType.MODIFY_PASSWD,
+ AuditLogOperation.CONTROL,
+ PrivilegeType.SECURITY,
+ result,
+ database,
+ null),
+ () -> targetUsername);
+ }
+
+ boolean isEnabled() {
+ return targetUsername != null;
+ }
+
+ @FunctionalInterface
+ interface AuditLogWriter {
+
+ void log(IAuditEntity auditEntity, Supplier<String> logMessage);
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditTask.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditTask.java
new file mode 100644
index 00000000000..f8080ee4a25
--- /dev/null
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditTask.java
@@ -0,0 +1,87 @@
+/*
+ * 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.audit;
+
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult;
+import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask;
+import
org.apache.iotdb.db.queryengine.plan.execution.config.executor.IConfigTaskExecutor;
+import org.apache.iotdb.rpc.RpcUtils;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import jakarta.validation.constraints.NotNull;
+
+/** Records the final result of a password-change config task without changing
its execution. */
+public final class PasswordChangeAuditTask implements IConfigTask {
+
+ private final IConfigTask delegate;
+ private final PasswordChangeAuditContext auditContext;
+
+ private PasswordChangeAuditTask(IConfigTask delegate,
PasswordChangeAuditContext auditContext) {
+ this.delegate = delegate;
+ this.auditContext = auditContext;
+ }
+
+ public static IConfigTask wrap(IConfigTask delegate,
PasswordChangeAuditContext auditContext) {
+ return auditContext.isEnabled()
+ ? new PasswordChangeAuditTask(delegate, auditContext)
+ : delegate;
+ }
+
+ @Override
+ public ListenableFuture<ConfigTaskResult> execute(IConfigTaskExecutor
configTaskExecutor)
+ throws InterruptedException {
+ try {
+ final ListenableFuture<ConfigTaskResult> future =
delegate.execute(configTaskExecutor);
+ Futures.addCallback(
+ future,
+ new FutureCallback<ConfigTaskResult>() {
+ @Override
+ public void onSuccess(ConfigTaskResult result) {
+ auditContext.log(toStatus(result));
+ }
+
+ @Override
+ public void onFailure(@NotNull Throwable throwable) {
+ auditContext.log(null);
+ }
+ },
+ MoreExecutors.directExecutor());
+ return future;
+ } catch (InterruptedException | RuntimeException | Error e) {
+ auditContext.log(null);
+ throw e;
+ }
+ }
+
+ private static TSStatus toStatus(ConfigTaskResult result) {
+ if (result == null) {
+ return null;
+ }
+ if (result.getStatus() != null) {
+ return result.getStatus();
+ }
+ return result.getStatusCode() == null ? null :
RpcUtils.getStatus(result.getStatusCode());
+ }
+}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java
index d3926bcac79..495ebc42b65 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java
@@ -80,6 +80,7 @@ public class MPPQueryContext implements IAuditEntity {
private static final Logger LOGGER =
LoggerFactory.getLogger(MPPQueryContext.class);
private String sql;
+ private String auditSqlString;
private final QueryId queryId;
/** The type of explanation for a query. */
@@ -222,6 +223,7 @@ public class MPPQueryContext implements IAuditEntity {
TEndPoint localInternalEndpoint) {
this(queryId);
this.sql = sql;
+ this.auditSqlString = sql;
this.session = session;
this.localQueryId = localQueryId;
this.localDataBlockEndpoint = localDataBlockEndpoint;
@@ -1040,12 +1042,12 @@ public class MPPQueryContext implements IAuditEntity {
@Override
public String getSqlString() {
- return sql;
+ return auditSqlString;
}
@Override
public IAuditEntity setSqlString(String sqlString) {
- // Do nothing
+ this.auditSqlString = sqlString;
return this;
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java
index 3e2eb129dc8..57a2bbd4479 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java
@@ -52,6 +52,8 @@ import
org.apache.iotdb.commons.schema.table.column.TsTableColumnCategory;
import org.apache.iotdb.commons.schema.table.column.TsTableColumnSchema;
import org.apache.iotdb.confignode.rpc.thrift.TDatabaseSchema;
import org.apache.iotdb.db.audit.DNAuditLogger;
+import org.apache.iotdb.db.audit.PasswordChangeAuditContext;
+import org.apache.iotdb.db.audit.PasswordChangeAuditTask;
import org.apache.iotdb.db.auth.AuthorityChecker;
import org.apache.iotdb.db.conf.IoTDBConfig;
import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
@@ -1626,18 +1628,30 @@ public class TableConfigTaskVisitor implements
AstVisitor<IConfigTask, MPPQueryC
@Override
public IConfigTask visitRelationalAuthorPlan(
RelationalAuthorStatement node, MPPQueryContext context) {
- context.setQueryType(node.getQueryType());
- node.setExecutedByUserId(context.getUserId());
- TSStatus status =
node.checkStatementIsValid(context.getSession().getUserName());
- if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
- throw new AccessDeniedException(status.getMessage());
- }
- accessControl.checkUserCanRunRelationalAuthorStatement(
- context.getSession().getUserName(), node, context);
- if (node.getAuthorType() == AuthorRType.UPDATE_USER) {
- visitUpdateUser(node);
+ PasswordChangeAuditContext auditContext =
+ PasswordChangeAuditContext.forTableStatement(node,
context.getSession());
+ boolean executionDelegated = false;
+ try {
+ context.setQueryType(node.getQueryType());
+ node.setExecutedByUserId(context.getUserId());
+ TSStatus status =
node.checkStatementIsValid(context.getSession().getUserName());
+ if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+ throw new AccessDeniedException(status.getMessage());
+ }
+ accessControl.checkUserCanRunRelationalAuthorStatement(
+ context.getSession().getUserName(), node, context);
+ if (node.getAuthorType() == AuthorRType.UPDATE_USER) {
+ visitUpdateUser(node);
+ }
+ IConfigTask task =
+ PasswordChangeAuditTask.wrap(new RelationalAuthorizerTask(node),
auditContext);
+ executionDelegated = true;
+ return task;
+ } finally {
+ if (!executionDelegated) {
+ auditContext.log(null);
+ }
}
- return new RelationalAuthorizerTask(node);
}
private void visitUpdateUser(RelationalAuthorStatement node) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java
index d0874e9cd21..573cf86b2cf 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java
@@ -27,6 +27,8 @@ import
org.apache.iotdb.commons.exception.auth.AccessDeniedException;
import org.apache.iotdb.commons.executable.ExecutableManager;
import org.apache.iotdb.commons.path.PartialPath;
import org.apache.iotdb.commons.pipe.config.constant.SystemConstant;
+import org.apache.iotdb.db.audit.PasswordChangeAuditContext;
+import org.apache.iotdb.db.audit.PasswordChangeAuditTask;
import org.apache.iotdb.db.auth.AuthorityChecker;
import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
@@ -339,7 +341,7 @@ public class TreeConfigTaskVisitor extends
StatementVisitor<IConfigTask, MPPQuer
public IConfigTask visitAuthor(AuthorStatement statement, MPPQueryContext
context) {
statement.setExecutedByUserId(context.getUserId());
if (statement.getAuthorType() == AuthorType.UPDATE_USER) {
- visitUpdateUser(statement);
+ return visitUpdateUser(statement, context);
}
if (statement.getAuthorType() == AuthorType.RENAME_USER) {
visitRenameUser(statement);
@@ -351,11 +353,27 @@ public class TreeConfigTaskVisitor extends
StatementVisitor<IConfigTask, MPPQuer
return new AuthorizerTask(statement);
}
- private void visitUpdateUser(AuthorStatement statement) {
- statement.setPassWord(
- AuthorityChecker.getAuthorityFetcher()
- .getUser(statement.getUserName(), true)
- .getPassword());
+ private IConfigTask visitUpdateUser(AuthorStatement statement,
MPPQueryContext context) {
+ PasswordChangeAuditContext auditContext =
+ PasswordChangeAuditContext.forTreeStatement(statement,
context.getSession());
+ boolean executionDelegated = false;
+ try {
+ statement.setPassWord(
+ AuthorityChecker.getAuthorityFetcher()
+ .getUser(statement.getUserName(), true)
+ .getPassword());
+ TSStatus status =
statement.checkStatementIsValid(context.getSession().getUserName());
+ if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+ throw new AccessDeniedException(status.getMessage());
+ }
+ IConfigTask task = PasswordChangeAuditTask.wrap(new
AuthorizerTask(statement), auditContext);
+ executionDelegated = true;
+ return task;
+ } finally {
+ if (!executionDelegated) {
+ auditContext.log(null);
+ }
+ }
}
private void visitRenameUser(AuthorStatement statement) {
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/AccessControlImpl.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/AccessControlImpl.java
index 9e7e9871807..bc20a7d972f 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/AccessControlImpl.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/AccessControlImpl.java
@@ -288,6 +288,9 @@ public class AccessControlImpl implements AccessControl {
return;
case RENAME_USER:
case UPDATE_USER:
+ if (type == AuthorRType.UPDATE_USER) {
+ auditEntity.setSqlString(null);
+ }
auditEntity.setAuditLogOperation(AuditLogOperation.DDL);
if (statement.getUserName().equals(userName)) {
// users can change the username and password of themselves
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java
index 9d5a9771059..73d8518a776 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java
@@ -34,6 +34,7 @@ import org.apache.iotdb.commons.schema.SchemaConstant;
import org.apache.iotdb.commons.schema.table.Audit;
import org.apache.iotdb.commons.utils.StatusUtils;
import org.apache.iotdb.db.audit.DNAuditLogger;
+import org.apache.iotdb.db.audit.PasswordChangeAuditContext;
import org.apache.iotdb.db.auth.AuthorityChecker;
import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
import org.apache.iotdb.db.queryengine.plan.statement.AuthorType;
@@ -569,30 +570,9 @@ public class TreeAccessCheckVisitor extends
StatementVisitor<TSStatus, TreeAcces
PrivilegeType.MANAGE_USER,
statement::getUserName);
case UPDATE_USER:
+ return checkCanUpdateUser(statement, context);
case RENAME_USER:
- context.setAuditLogOperation(AuditLogOperation.DDL);
- if (statement.getUserName().equals(context.getUsername())) {
- // users can change the username and password of themselves
- AUDIT_LOGGER.recordObjectAuthenticationAuditLog(
- context.setResult(true), context::getUsername);
- return RpcUtils.SUCCESS_STATUS;
- }
- if (AuthorityChecker.SUPER_USER_ID
- ==
AuthorityChecker.getUserId(statement.getUserName()).orElse(-1L)) {
- // Only the superuser can alter him/herself
- AUDIT_LOGGER.recordObjectAuthenticationAuditLog(
- context.setResult(false), context::getUsername);
- return AuthorityChecker.getTSStatus(
- false,
- "Has no permission to execute "
- + authorType
- + ", because only the superuser can alter him/herself.");
- }
- context.setPrivilegeType(PrivilegeType.SECURITY);
- return checkGlobalAuth(
- context.setAuditLogOperation(AuditLogOperation.DDL),
- PrivilegeType.MANAGE_USER,
- statement::getUserName);
+ return checkCanAlterUser(statement, context);
case LIST_USER:
context.setAuditLogOperation(AuditLogOperation.QUERY).setResult(true);
if (checkHasGlobalAuth(
@@ -698,6 +678,48 @@ public class TreeAccessCheckVisitor extends
StatementVisitor<TSStatus, TreeAcces
}
}
+ private TSStatus checkCanUpdateUser(AuthorStatement statement,
TreeAccessCheckContext context) {
+ context.setSqlString(null);
+ PasswordChangeAuditContext auditContext =
+ PasswordChangeAuditContext.forTreeAuthorization(statement, context);
+ TSStatus status = null;
+ try {
+ status = checkCanAlterUser(statement, context);
+ return status;
+ } finally {
+ if (status == null || status.getCode() !=
TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+ auditContext.log(status);
+ }
+ }
+ }
+
+ private TSStatus checkCanAlterUser(AuthorStatement statement,
TreeAccessCheckContext context) {
+ context.setAuditLogOperation(AuditLogOperation.DDL);
+ if (statement.getUserName().equals(context.getUsername())) {
+ // users can change the username and password of themselves
+ AUDIT_LOGGER.recordObjectAuthenticationAuditLog(
+ context.setResult(true), context::getUsername);
+ return RpcUtils.SUCCESS_STATUS;
+ }
+ if (AuthorityChecker.SUPER_USER_ID
+ == AuthorityChecker.getUserId(statement.getUserName()).orElse(-1L)) {
+ // Only the superuser can alter him/herself
+ AUDIT_LOGGER.recordObjectAuthenticationAuditLog(
+ context.setResult(false), context::getUsername);
+ return AuthorityChecker.getTSStatus(
+ false,
+ String.format(
+ DataNodeQueryMessages
+
.EXCEPTION_HAS_NO_PERMISSION_TO_EXECUTE_ARG_BECAUSE_ONLY_THE_SUPERUSER_CAN_ALTER_HIM_HERSELF_C5902893,
+ statement.getAuthorType()));
+ }
+ context.setPrivilegeType(PrivilegeType.SECURITY);
+ return checkGlobalAuth(
+ context.setAuditLogOperation(AuditLogOperation.DDL),
+ PrivilegeType.MANAGE_USER,
+ statement::getUserName);
+ }
+
// =================================== CQ related
====================================
@Override
public TSStatus visitCreateContinuousQuery(
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerTest.java
new file mode 100644
index 00000000000..26efc9ece37
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.audit;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.commons.audit.AuditEventType;
+import org.apache.iotdb.commons.audit.AuditLogOperation;
+import org.apache.iotdb.commons.audit.IAuditEntity;
+import org.apache.iotdb.commons.audit.UserEntity;
+import org.apache.iotdb.commons.auth.entity.PrivilegeType;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.queryengine.common.SessionInfo;
+import org.apache.iotdb.commons.queryengine.common.SqlDialect;
+import org.apache.iotdb.db.protocol.session.IClientSession;
+import org.apache.iotdb.db.protocol.session.InternalClientSession;
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator;
+import
org.apache.iotdb.db.queryengine.plan.relational.security.AccessControlImpl;
+import
org.apache.iotdb.db.queryengine.plan.relational.security.ITableAuthChecker;
+import
org.apache.iotdb.db.queryengine.plan.relational.security.TreeAccessCheckContext;
+import
org.apache.iotdb.db.queryengine.plan.relational.security.TreeAccessCheckVisitor;
+import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RelationalAuthorStatement;
+import org.apache.iotdb.db.queryengine.plan.relational.sql.parser.SqlParser;
+import org.apache.iotdb.db.queryengine.plan.relational.type.AuthorRType;
+import org.apache.iotdb.db.queryengine.plan.statement.Statement;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
+import org.apache.iotdb.db.queryengine.plan.statement.sys.AuthorStatement;
+
+import org.apache.tsfile.common.conf.TSFileConfig;
+import org.apache.tsfile.utils.Binary;
+import org.junit.Test;
+
+import java.time.ZoneId;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+public class DNAuditLoggerTest {
+
+ @Test
+ public void testAlterUserPasswordSqlIsSanitized() {
+ assertEquals(
+ "alter user alice set password ...",
+ DNAuditLogger.sanitizeAuditSql("alter user alice set password
'secret'"));
+ assertEquals(
+ "ALTER USER\talice\nSET PASSWORD\t...;",
+ DNAuditLogger.sanitizeAuditSql("ALTER USER\talice\nSET
PASSWORD\t\"Secret@123456\";"));
+ assertEquals(
+ "ALTER USER alice SET PASSWORD ...;",
+ DNAuditLogger.sanitizeAuditSql("ALTER USER alice SET PASSWORD
'pass''word';"));
+ assertEquals(
+ "ALTER USER alice SET PASSWORD ...",
+ DNAuditLogger.sanitizeAuditSql("ALTER USER alice SET PASSWORD
\"pass\"\"word\""));
+ assertEquals(
+ "ALTER USER alice SET PASSWORD ...;",
+ DNAuditLogger.sanitizeAuditSql("ALTER USER alice SET PASSWORD
U&'pass\\0077ord';"));
+ }
+
+ @Test
+ public void testNonPasswordAlterUserSqlIsNotSanitized() {
+ assertSqlIsUnchanged("ALTER USER alice RENAME TO bob;");
+ assertSqlIsUnchanged("ALTER USER alice ACCOUNT UNLOCK;");
+ assertSqlIsUnchanged("ALTER USER alice SET MAX_SESSION_PER_USER 10;");
+ assertSqlIsUnchanged("ALTER USER alice SET MIN_SESSION_PER_USER 1;");
+ }
+
+ @Test
+ public void testExistingAuditSqlSanitizationIsPreserved() {
+ assertNull(DNAuditLogger.sanitizeAuditSql(null));
+ assertEquals(
+ "CREATE USER alice ...",
+ DNAuditLogger.sanitizeAuditSql("CREATE USER alice 'Secret@123456'"));
+ assertEquals(
+ "INSERT INTO root.sg.d(time, s) values(...)",
+ DNAuditLogger.sanitizeAuditSql("INSERT INTO root.sg.d(time, s)
values(1, 'secret')"));
+ }
+
+ @Test
+ public void testTreeObjectAuthenticationSqlIsSanitizedBeforePersistence()
throws Exception {
+ String originalSql = "ALTER USER alice SET PASSWORD 'treeSecret';";
+ Statement statement = StatementGenerator.createStatement(originalSql,
ZoneId.systemDefault());
+ assertTrue(statement instanceof AuthorStatement);
+ TreeAccessCheckContext auditEntity =
+ (TreeAccessCheckContext)
+ new TreeAccessCheckContext(7L, "alice",
"127.0.0.1").setSqlString(originalSql);
+
+ statement.accept(new TreeAccessCheckVisitor(), auditEntity);
+
+ assertPersistedAuditSql(objectAuthenticationAuditEntity(auditEntity,
true), "null");
+ }
+
+ @Test
+ public void
testTableObjectAuthenticationSqlIsRemovedAfterParsingBeforePersistence()
+ throws Exception {
+ assertTablePasswordSqlIsRemoved("ALTER USER alice SET PASSWORD 'Secret' --
rotate", "Secret");
+ assertTablePasswordSqlIsRemoved(
+ "ALTER USER alice SET PASSWORD 'Secret' /* rotate */", "Secret");
+ assertTablePasswordSqlIsRemoved(
+ "ALTER USER alice SET PASSWORD U&'Secret' UESCAPE '#'", "Secret");
+ }
+
+ private static void assertTablePasswordSqlIsRemoved(String originalSql,
String password)
+ throws Exception {
+ IClientSession clientSession = new
InternalClientSession("table_audit_test");
+ clientSession.setDatabaseName("database");
+ org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement
parsedStatement =
+ new SqlParser().createStatement(originalSql, ZoneId.systemDefault(),
clientSession);
+ assertTrue(parsedStatement instanceof RelationalAuthorStatement);
+ RelationalAuthorStatement authorStatement = (RelationalAuthorStatement)
parsedStatement;
+ assertEquals(AuthorRType.UPDATE_USER, authorStatement.getAuthorType());
+
+ MPPQueryContext queryContext =
+ new MPPQueryContext(
+ originalSql,
+ new QueryId("table_audit_test"),
+ new SessionInfo(
+ 1L,
+ new UserEntity(7L, "alice", "127.0.0.1"),
+ ZoneId.systemDefault(),
+ "database",
+ SqlDialect.TABLE),
+ new TEndPoint(),
+ new TEndPoint());
+ new AccessControlImpl(mock(ITableAuthChecker.class), new
TreeAccessCheckVisitor())
+ .checkUserCanRunRelationalAuthorStatement("alice", authorStatement,
queryContext);
+
+ assertEquals(originalSql, queryContext.getSql());
+ assertNull(queryContext.getSqlString());
+ String persistedSql =
getPersistedAuditSql(objectAuthenticationAuditEntity(queryContext, true));
+ assertEquals("null", persistedSql);
+ assertFalse(persistedSql.contains(password));
+ }
+
+ private static void assertSqlIsUnchanged(String sql) {
+ assertEquals(sql, DNAuditLogger.sanitizeAuditSql(sql));
+ }
+
+ private static IAuditEntity objectAuthenticationAuditEntity(
+ IAuditEntity auditEntity, boolean result) {
+ return auditEntity
+ .setAuditEventType(AuditEventType.OBJECT_AUTHENTICATION)
+ .setAuditLogOperation(AuditLogOperation.DDL)
+ .setPrivilegeType(PrivilegeType.SECURITY)
+ .setResult(result)
+ .setDatabase("database");
+ }
+
+ private static void assertPersistedAuditSql(IAuditEntity auditEntity, String
expectedSql)
+ throws Exception {
+ assertEquals(expectedSql, getPersistedAuditSql(auditEntity));
+ }
+
+ private static String getPersistedAuditSql(IAuditEntity auditEntity) throws
Exception {
+ InsertRowStatement statement =
+ DNAuditLogger.generateInsertStatement(
+ auditEntity, "alice", new
PartialPath("root.__audit.log.node_0.u_7"), 1L);
+
+ return ((Binary)
statement.getValues()[8]).getStringValue(TSFileConfig.STRING_CHARSET);
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/PasswordChangeAuditContextTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/PasswordChangeAuditContextTest.java
new file mode 100644
index 00000000000..ac340762e8c
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/PasswordChangeAuditContextTest.java
@@ -0,0 +1,327 @@
+/*
+ * 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.audit;
+
+import org.apache.iotdb.commons.audit.AuditEventType;
+import org.apache.iotdb.commons.audit.AuditLogOperation;
+import org.apache.iotdb.commons.audit.IAuditEntity;
+import org.apache.iotdb.commons.audit.UserEntity;
+import org.apache.iotdb.commons.auth.entity.PrivilegeType;
+import org.apache.iotdb.commons.queryengine.common.SessionInfo;
+import org.apache.iotdb.commons.queryengine.common.SqlDialect;
+import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult;
+import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask;
+import
org.apache.iotdb.db.queryengine.plan.relational.security.TreeAccessCheckContext;
+import
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RelationalAuthorStatement;
+import org.apache.iotdb.db.queryengine.plan.relational.type.AuthorRType;
+import org.apache.iotdb.db.queryengine.plan.statement.AuthorType;
+import org.apache.iotdb.db.queryengine.plan.statement.sys.AuthorStatement;
+import org.apache.iotdb.rpc.RpcUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import com.google.common.util.concurrent.SettableFuture;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+public class PasswordChangeAuditContextTest {
+
+ @Test
+ public void testTreePasswordChangeSuccess() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ AuthorStatement statement = treePasswordChangeStatement();
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(statement, sessionInfo(),
auditLogWriter);
+
+ context.log(RpcUtils.SUCCESS_STATUS);
+
+ assertAuditLog(auditLogWriter, true);
+ }
+
+ @Test
+ public void testTablePasswordChangeFailure() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ RelationalAuthorStatement statement =
+ new RelationalAuthorStatement(
+ AuthorRType.UPDATE_USER,
+ "target_user",
+ null,
+ null,
+ null,
+ Collections.emptySet(),
+ false,
+ null);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTableStatement(
+ statement, sessionInfo(SqlDialect.TABLE), auditLogWriter);
+
+ context.log(RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR));
+
+ assertAuditLog(auditLogWriter, false);
+ }
+
+ @Test
+ public void testOtherAuthorStatementIsIgnored() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(
+ new AuthorStatement(AuthorType.CREATE_USER), sessionInfo(),
auditLogWriter);
+
+ context.log(RpcUtils.SUCCESS_STATUS);
+
+ verify(auditLogWriter, never()).log(any(), any());
+ }
+
+ @Test
+ public void testRenameUserStatementIsIgnored() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeAuthorization(
+ new AuthorStatement(AuthorType.RENAME_USER),
+ new TreeAccessCheckContext(7L, "operator", "127.0.0.1"),
+ auditLogWriter);
+
+ context.log(null);
+
+ verify(auditLogWriter, never()).log(any(), any());
+ }
+
+ @Test
+ public void testDeniedTreePasswordChangeUsesAuthorizationIdentity() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ AuthorStatement statement = treePasswordChangeStatement();
+ TreeAccessCheckContext authorizationContext =
+ new TreeAccessCheckContext(7L, "operator", "127.0.0.1");
+ authorizationContext.setDatabase("database");
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeAuthorization(
+ statement, authorizationContext, auditLogWriter);
+
+ context.log(null);
+
+ assertAuditLog(auditLogWriter, false);
+ }
+
+ @Test
+ public void testPasswordChangeAuditIsRecordedOnlyOnce() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(
+ treePasswordChangeStatement(), sessionInfo(), auditLogWriter);
+
+ context.log(RpcUtils.SUCCESS_STATUS);
+ context.log(RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR));
+
+ assertAuditLog(auditLogWriter, true);
+ }
+
+ @Test
+ public void testPasswordChangeAuditTaskRecordsSuccess() throws Exception {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(
+ treePasswordChangeStatement(), sessionInfo(), auditLogWriter);
+ SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+ IConfigTask delegate = ignored -> future;
+ IConfigTask task = PasswordChangeAuditTask.wrap(delegate, context);
+
+ assertSame(future, task.execute(null));
+ future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS));
+
+ assertAuditLog(auditLogWriter, true);
+ }
+
+ @Test
+ public void testPasswordChangeAuditTaskRecordsAsynchronousFailure() throws
Exception {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTableStatement(
+ tablePasswordChangeStatement(), sessionInfo(SqlDialect.TABLE),
auditLogWriter);
+ SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+ IConfigTask task = PasswordChangeAuditTask.wrap(ignored -> future,
context);
+
+ task.execute(null);
+ future.setException(new RuntimeException());
+
+ assertAuditLog(auditLogWriter, false);
+ }
+
+ @Test
+ public void testPasswordChangeAuditTaskRecordsFailedResult() throws
Exception {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(
+ treePasswordChangeStatement(), sessionInfo(), auditLogWriter);
+ SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+ IConfigTask task = PasswordChangeAuditTask.wrap(ignored -> future,
context);
+
+ task.execute(null);
+ future.set(new
ConfigTaskResult(RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR)));
+
+ assertAuditLog(auditLogWriter, false);
+ }
+
+ @Test
+ public void testPasswordChangeAuditTaskTreatsRedirectionAsSuccess() throws
Exception {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(
+ treePasswordChangeStatement(), sessionInfo(), auditLogWriter);
+ SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+ IConfigTask task = PasswordChangeAuditTask.wrap(ignored -> future,
context);
+
+ task.execute(null);
+ future.set(new ConfigTaskResult(TSStatusCode.REDIRECTION_RECOMMEND));
+
+ assertAuditLog(auditLogWriter, true);
+ }
+
+ @Test
+ public void testPasswordChangeAuditTaskRecordsSynchronousFailure() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(
+ treePasswordChangeStatement(), sessionInfo(), auditLogWriter);
+ IConfigTask task =
+ PasswordChangeAuditTask.wrap(
+ ignored -> {
+ throw new IllegalStateException();
+ },
+ context);
+
+ assertThrows(IllegalStateException.class, () -> task.execute(null));
+
+ assertAuditLog(auditLogWriter, false);
+ }
+
+ @Test
+ public void testPasswordChangeAuditTaskRecordsInterruption() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(
+ treePasswordChangeStatement(), sessionInfo(), auditLogWriter);
+ IConfigTask task =
+ PasswordChangeAuditTask.wrap(
+ ignored -> {
+ throw new InterruptedException();
+ },
+ context);
+
+ assertThrows(InterruptedException.class, () -> task.execute(null));
+
+ assertAuditLog(auditLogWriter, false);
+ }
+
+ @Test
+ public void testPasswordChangeAuditTaskDoesNotWrapOtherStatements() {
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter =
+ mock(PasswordChangeAuditContext.AuditLogWriter.class);
+ PasswordChangeAuditContext context =
+ PasswordChangeAuditContext.forTreeStatement(
+ new AuthorStatement(AuthorType.RENAME_USER), sessionInfo(),
auditLogWriter);
+ IConfigTask delegate = ignored -> SettableFuture.create();
+
+ assertSame(delegate, PasswordChangeAuditTask.wrap(delegate, context));
+ }
+
+ private static SessionInfo sessionInfo() {
+ return sessionInfo(SqlDialect.TREE);
+ }
+
+ private static SessionInfo sessionInfo(SqlDialect sqlDialect) {
+ return new SessionInfo(
+ 1L,
+ new UserEntity(7L, "operator", "127.0.0.1"),
+ ZoneId.systemDefault(),
+ "database",
+ sqlDialect);
+ }
+
+ private static AuthorStatement treePasswordChangeStatement() {
+ return new AuthorStatement(AuthorType.UPDATE_USER) {
+ @Override
+ public String getUserName() {
+ return "target_user";
+ }
+ };
+ }
+
+ private static RelationalAuthorStatement tablePasswordChangeStatement() {
+ return new RelationalAuthorStatement(
+ AuthorRType.UPDATE_USER,
+ "target_user",
+ null,
+ null,
+ null,
+ Collections.emptySet(),
+ false,
+ null);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void assertAuditLog(
+ PasswordChangeAuditContext.AuditLogWriter auditLogWriter, boolean
expectedResult) {
+ ArgumentCaptor<IAuditEntity> entityCaptor =
ArgumentCaptor.forClass(IAuditEntity.class);
+ ArgumentCaptor<Supplier<String>> logCaptor =
ArgumentCaptor.forClass(Supplier.class);
+ verify(auditLogWriter).log(entityCaptor.capture(), logCaptor.capture());
+
+ IAuditEntity entity = entityCaptor.getValue();
+ assertEquals(7L, entity.getUserId());
+ assertEquals("operator", entity.getUsername());
+ assertEquals("127.0.0.1", entity.getCliHostname());
+ assertEquals(AuditEventType.MODIFY_PASSWD, entity.getAuditEventType());
+ assertEquals(AuditLogOperation.CONTROL, entity.getAuditLogOperation());
+ assertEquals(PrivilegeType.SECURITY, entity.getPrivilegeTypes().get(0));
+ if (expectedResult) {
+ assertTrue(entity.getResult());
+ } else {
+ assertFalse(entity.getResult());
+ }
+ assertEquals("database", entity.getDatabase());
+ assertNull(entity.getSqlString());
+ assertEquals("target_user", logCaptor.getValue().get());
+ }
+}