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());
+  }
+}

Reply via email to