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 c46c0732bb3 Record audit logs for normal and failed role grants and 
revokes (#18355)
c46c0732bb3 is described below

commit c46c0732bb3542a0543cdd56e312161f2266dc0b
Author: libo <[email protected]>
AuthorDate: Wed Aug 5 12:08:27 2026 +0800

    Record audit logs for normal and failed role grants and revokes (#18355)
    
    * Record audit logs for failed role grants
    
    * Fix FMT_SMR.2 user role audit logging
    
    * Refine FMT_SMR.2 role membership audit logging
    
    * Move role membership audit into authorizer tasks
---
 .../apache/iotdb/db/i18n/DataNodeMiscMessages.java |   1 +
 .../apache/iotdb/db/i18n/DataNodeMiscMessages.java |   1 +
 .../org/apache/iotdb/db/audit/DNAuditLogger.java   | 128 ++++++++
 .../db/audit/UserRoleModificationAuditContext.java | 184 +++++++++++
 .../execution/config/TableConfigTaskVisitor.java   |  12 +-
 .../execution/config/TreeConfigTaskVisitor.java    |  37 ++-
 .../relational/RelationalAuthorizerTask.java       |   9 +-
 .../plan/execution/config/sys/AuthorizerTask.java  |  11 +-
 .../security/TreeAccessCheckVisitor.java           |  14 +-
 .../DNAuditLoggerUserRoleModificationTest.java     | 354 +++++++++++++++++++++
 .../apache/iotdb/commons/audit/AuditEventType.java |   1 +
 11 files changed, 731 insertions(+), 21 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
index b43037206cc..f3e8154dcde 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
@@ -121,6 +121,7 @@ public final class DataNodeMiscMessages {
 
   public static final String CREATE_NEW_REGION_ERROR_FMT = "create new region 
%s error,  exception:%s";
   public static final String CREATE_NEW_REGION_SUCCEED_FMT = "create new 
region %s succeed";
+  public static final String LOG_USER_ARG_ROLE_ARG_422D48D3 = "user: %s, role: 
%s";
   private DataNodeMiscMessages() {}
 
   // 
---------------------------------------------------------------------------
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
index a319bce5000..5dd49c8804e 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeMiscMessages.java
@@ -121,6 +121,7 @@ public final class DataNodeMiscMessages {
 
   public static final String CREATE_NEW_REGION_ERROR_FMT = "创建新 region %s 
错误,异常:%s";
   public static final String CREATE_NEW_REGION_SUCCEED_FMT = "创建新 region %s 
成功";
+  public static final String LOG_USER_ARG_ROLE_ARG_422D48D3 = "用户:%s,角色:%s";
   private DataNodeMiscMessages() {}
 
   // 
---------------------------------------------------------------------------
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 02fedbd5e1f..96fe1b0446a 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
@@ -31,6 +31,7 @@ import 
org.apache.iotdb.commons.exception.IllegalPathException;
 import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.queryengine.common.SessionInfo;
 import org.apache.iotdb.commons.utils.CommonDateTimeUtils;
+import org.apache.iotdb.db.i18n.DataNodeMiscMessages;
 import org.apache.iotdb.db.queryengine.plan.Coordinator;
 import 
org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RelationalAuthorStatement;
 import org.apache.iotdb.db.queryengine.plan.relational.type.AuthorRType;
@@ -213,6 +214,32 @@ public class DNAuditLogger extends AbstractAuditLogger {
         status);
   }
 
+  public void logUserRoleModificationAuthorizationFailure(
+      Statement statement, IAuditEntity auditEntity, @Nullable TSStatus 
status) {
+    if (isSuccessful(status)) {
+      // Successful authorization only allows the role modification to 
proceed. The actual
+      // modification result is audited by AuthorizerTask after execution.
+      return;
+    }
+    logUserRoleModification(getUserRoleTarget(statement), auditEntity, status);
+  }
+
+  public void logUserRoleModification(
+      Statement statement,
+      SessionInfo sessionInfo,
+      @Nullable String sql,
+      @Nullable TSStatus status) {
+    logUserRoleModification(getUserRoleTarget(statement), sessionInfo, sql, 
status);
+  }
+
+  public void logUserRoleModification(
+      org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement 
statement,
+      SessionInfo sessionInfo,
+      @Nullable String sql,
+      @Nullable TSStatus status) {
+    logUserRoleModification(getUserRoleTarget(statement), sessionInfo, sql, 
status);
+  }
+
   public void logRevokeFailure(
       Statement statement,
       SessionInfo sessionInfo,
@@ -272,6 +299,65 @@ public class DNAuditLogger extends AbstractAuditLogger {
         () -> targetName);
   }
 
+  private void logUserRoleModification(
+      @Nullable UserRoleTarget target,
+      @Nullable SessionInfo sessionInfo,
+      @Nullable String sql,
+      @Nullable TSStatus status) {
+    if (target == null || sessionInfo == null || isRedirected(status)) {
+      return;
+    }
+    logUserRoleModification(
+        target,
+        sessionInfo.getUserId(),
+        sessionInfo.getUserName(),
+        sessionInfo.getCliHostname(),
+        sessionInfo.getDatabaseName().orElse(null),
+        sql,
+        status);
+  }
+
+  private void logUserRoleModification(
+      @Nullable UserRoleTarget target, IAuditEntity auditEntity, @Nullable 
TSStatus status) {
+    if (target == null || isRedirected(status)) {
+      return;
+    }
+    logUserRoleModification(
+        target,
+        auditEntity.getUserId(),
+        auditEntity.getUsername(),
+        auditEntity.getCliHostname(),
+        auditEntity.getDatabase(),
+        auditEntity.getSqlString(),
+        status);
+  }
+
+  private void logUserRoleModification(
+      UserRoleTarget target,
+      long userId,
+      String username,
+      String clientAddress,
+      @Nullable String database,
+      @Nullable String sql,
+      @Nullable TSStatus status) {
+    log(
+        new AuditLogFields(
+            userId,
+            username,
+            clientAddress,
+            AuditEventType.MODIFY_ROLE_MEMBERSHIP,
+            AuditLogOperation.CONTROL,
+            PrivilegeType.SECURITY,
+            isSuccessful(status),
+            database,
+            sql),
+        () ->
+            String.format(
+                DataNodeMiscMessages.LOG_USER_ARG_ROLE_ARG_422D48D3,
+                target.username,
+                target.roleName));
+  }
+
   @Nullable
   private static String getTargetName(Statement statement) {
     if (!(statement instanceof AuthorStatement)) {
@@ -314,12 +400,54 @@ public class DNAuditLogger extends AbstractAuditLogger {
     return null;
   }
 
+  @Nullable
+  private static UserRoleTarget getUserRoleTarget(Statement statement) {
+    if (!(statement instanceof AuthorStatement)) {
+      return null;
+    }
+    AuthorStatement authorStatement = (AuthorStatement) statement;
+    if (authorStatement.getAuthorType() != AuthorType.GRANT_USER_ROLE
+        && authorStatement.getAuthorType() != AuthorType.REVOKE_USER_ROLE) {
+      return null;
+    }
+    return new UserRoleTarget(authorStatement.getUserName(), 
authorStatement.getRoleName());
+  }
+
+  @Nullable
+  private static UserRoleTarget getUserRoleTarget(
+      org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement 
statement) {
+    if (!(statement instanceof RelationalAuthorStatement)) {
+      return null;
+    }
+    RelationalAuthorStatement authorStatement = (RelationalAuthorStatement) 
statement;
+    if (authorStatement.getAuthorType() != AuthorRType.GRANT_USER_ROLE
+        && authorStatement.getAuthorType() != AuthorRType.REVOKE_USER_ROLE) {
+      return null;
+    }
+    return new UserRoleTarget(authorStatement.getUserName(), 
authorStatement.getRoleName());
+  }
+
   private static boolean isSuccessful(@Nullable TSStatus status) {
     return status != null
         && (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()
             || status.getCode() == 
TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode());
   }
 
+  private static boolean isRedirected(@Nullable TSStatus status) {
+    return status != null && status.getCode() == 
TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode();
+  }
+
+  private static class UserRoleTarget {
+
+    private final String username;
+    private final String roleName;
+
+    private UserRoleTarget(String username, String roleName) {
+      this.username = username;
+      this.roleName = roleName;
+    }
+  }
+
   private static class DNAuditLoggerHolder {
 
     private static final DNAuditLogger INSTANCE = new DNAuditLogger();
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/UserRoleModificationAuditContext.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/UserRoleModificationAuditContext.java
new file mode 100644
index 00000000000..ed7556b44cb
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/UserRoleModificationAuditContext.java
@@ -0,0 +1,184 @@
+/*
+ * 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.exception.IoTDBException;
+import org.apache.iotdb.commons.exception.IoTDBRuntimeException;
+import org.apache.iotdb.commons.queryengine.common.SessionInfo;
+import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult;
+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.RpcUtils;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+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;
+
+import javax.annotation.Nullable;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+
+/** Ensures one user-role modification attempt produces at most one execution 
audit record. */
+public final class UserRoleModificationAuditContext {
+
+  private static final UserRoleModificationAuditContext EMPTY =
+      new UserRoleModificationAuditContext(null);
+
+  private final AuditLogWriter auditLogWriter;
+  private final AtomicBoolean logged = new AtomicBoolean(false);
+
+  private UserRoleModificationAuditContext(@Nullable AuditLogWriter 
auditLogWriter) {
+    this.auditLogWriter = auditLogWriter;
+  }
+
+  public static UserRoleModificationAuditContext empty() {
+    return EMPTY;
+  }
+
+  public static UserRoleModificationAuditContext forTreeStatement(
+      Statement statement, @Nullable SessionInfo sessionInfo, @Nullable String 
sql) {
+    return forTreeStatement(
+        statement,
+        sessionInfo,
+        status ->
+            DNAuditLogger.getInstance()
+                .logUserRoleModification(statement, sessionInfo, sql, status));
+  }
+
+  static UserRoleModificationAuditContext forTreeStatement(
+      Statement statement, @Nullable SessionInfo sessionInfo, AuditLogWriter 
auditLogWriter) {
+    return isUserRoleModification(statement) && sessionInfo != null
+        ? new UserRoleModificationAuditContext(auditLogWriter)
+        : EMPTY;
+  }
+
+  public static UserRoleModificationAuditContext forTableStatement(
+      org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement 
statement,
+      @Nullable SessionInfo sessionInfo,
+      @Nullable String sql) {
+    return forTableStatement(
+        statement,
+        sessionInfo,
+        status ->
+            DNAuditLogger.getInstance()
+                .logUserRoleModification(statement, sessionInfo, sql, status));
+  }
+
+  static UserRoleModificationAuditContext forTableStatement(
+      org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement 
statement,
+      @Nullable SessionInfo sessionInfo,
+      AuditLogWriter auditLogWriter) {
+    return isUserRoleModification(statement) && sessionInfo != null
+        ? new UserRoleModificationAuditContext(auditLogWriter)
+        : EMPTY;
+  }
+
+  public void log(@Nullable TSStatus status) {
+    if (auditLogWriter == null || isRedirected(status) || 
!logged.compareAndSet(false, true)) {
+      return;
+    }
+    auditLogWriter.log(status);
+  }
+
+  /** Executes the actual role membership modification and audits its final 
success or failure. */
+  public ListenableFuture<ConfigTaskResult> executeAndAudit(
+      Supplier<ListenableFuture<ConfigTaskResult>> operation) {
+    try {
+      ListenableFuture<ConfigTaskResult> future = operation.get();
+      if (auditLogWriter == null) {
+        return future;
+      }
+      Futures.addCallback(
+          future,
+          new FutureCallback<ConfigTaskResult>() {
+            @Override
+            public void onSuccess(ConfigTaskResult result) {
+              log(toStatus(result));
+            }
+
+            @Override
+            public void onFailure(@NotNull Throwable throwable) {
+              log(toStatus(throwable));
+            }
+          },
+          MoreExecutors.directExecutor());
+      return future;
+    } catch (RuntimeException | Error e) {
+      log(toStatus(e));
+      throw e;
+    }
+  }
+
+  private static boolean isUserRoleModification(Statement statement) {
+    if (!(statement instanceof AuthorStatement)) {
+      return false;
+    }
+    AuthorType type = ((AuthorStatement) statement).getAuthorType();
+    return type == AuthorType.GRANT_USER_ROLE || type == 
AuthorType.REVOKE_USER_ROLE;
+  }
+
+  private static boolean isUserRoleModification(
+      org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement 
statement) {
+    if (!(statement instanceof RelationalAuthorStatement)) {
+      return false;
+    }
+    AuthorRType type = ((RelationalAuthorStatement) statement).getAuthorType();
+    return type == AuthorRType.GRANT_USER_ROLE || type == 
AuthorRType.REVOKE_USER_ROLE;
+  }
+
+  private static boolean isRedirected(@Nullable TSStatus status) {
+    return status != null && status.getCode() == 
TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode();
+  }
+
+  private static TSStatus toStatus(@Nullable ConfigTaskResult result) {
+    if (result == null) {
+      return null;
+    }
+    if (result.getStatus() != null) {
+      return result.getStatus();
+    }
+    return result.getStatusCode() == null ? null : 
RpcUtils.getStatus(result.getStatusCode());
+  }
+
+  private static TSStatus toStatus(Throwable throwable) {
+    if (throwable instanceof IoTDBException) {
+      return ((IoTDBException) throwable).getStatus();
+    }
+    if (throwable instanceof IoTDBRuntimeException) {
+      return ((IoTDBRuntimeException) throwable).getStatus();
+    }
+    return null;
+  }
+
+  @FunctionalInterface
+  interface AuditLogWriter {
+
+    void log(@Nullable TSStatus status);
+  }
+}
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 57a2bbd4479..5f0ff823972 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
@@ -54,6 +54,7 @@ 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.audit.UserRoleModificationAuditContext;
 import org.apache.iotdb.db.auth.AuthorityChecker;
 import org.apache.iotdb.db.conf.IoTDBConfig;
 import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
@@ -1628,8 +1629,11 @@ public class TableConfigTaskVisitor implements 
AstVisitor<IConfigTask, MPPQueryC
   @Override
   public IConfigTask visitRelationalAuthorPlan(
       RelationalAuthorStatement node, MPPQueryContext context) {
-    PasswordChangeAuditContext auditContext =
+    PasswordChangeAuditContext passwordAuditContext =
         PasswordChangeAuditContext.forTableStatement(node, 
context.getSession());
+    UserRoleModificationAuditContext userRoleAuditContext =
+        UserRoleModificationAuditContext.forTableStatement(
+            node, context.getSession(), context.getSql());
     boolean executionDelegated = false;
     try {
       context.setQueryType(node.getQueryType());
@@ -1644,12 +1648,14 @@ public class TableConfigTaskVisitor implements 
AstVisitor<IConfigTask, MPPQueryC
         visitUpdateUser(node);
       }
       IConfigTask task =
-          PasswordChangeAuditTask.wrap(new RelationalAuthorizerTask(node), 
auditContext);
+          PasswordChangeAuditTask.wrap(
+              new RelationalAuthorizerTask(node, userRoleAuditContext), 
passwordAuditContext);
       executionDelegated = true;
       return task;
     } finally {
       if (!executionDelegated) {
-        auditContext.log(null);
+        passwordAuditContext.log(null);
+        userRoleAuditContext.log(null);
       }
     }
   }
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 573cf86b2cf..d1e363f81e1 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
@@ -29,6 +29,7 @@ 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.audit.UserRoleModificationAuditContext;
 import org.apache.iotdb.db.auth.AuthorityChecker;
 import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
@@ -339,18 +340,32 @@ public class TreeConfigTaskVisitor extends 
StatementVisitor<IConfigTask, MPPQuer
 
   @Override
   public IConfigTask visitAuthor(AuthorStatement statement, MPPQueryContext 
context) {
-    statement.setExecutedByUserId(context.getUserId());
-    if (statement.getAuthorType() == AuthorType.UPDATE_USER) {
-      return visitUpdateUser(statement, context);
-    }
-    if (statement.getAuthorType() == AuthorType.RENAME_USER) {
-      visitRenameUser(statement);
-    }
-    TSStatus status = 
statement.checkStatementIsValid(context.getSession().getUserName());
-    if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
-      throw new AccessDeniedException(status.getMessage());
+    UserRoleModificationAuditContext auditContext =
+        UserRoleModificationAuditContext.forTreeStatement(
+            statement, context.getSession(), context.getSql());
+    boolean executionDelegated = false;
+    try {
+      statement.setExecutedByUserId(context.getUserId());
+      if (statement.getAuthorType() == AuthorType.UPDATE_USER) {
+        IConfigTask task = visitUpdateUser(statement, context);
+        executionDelegated = true;
+        return task;
+      }
+      if (statement.getAuthorType() == AuthorType.RENAME_USER) {
+        visitRenameUser(statement);
+      }
+      TSStatus status = 
statement.checkStatementIsValid(context.getSession().getUserName());
+      if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) {
+        throw new AccessDeniedException(status.getMessage());
+      }
+      IConfigTask task = new AuthorizerTask(statement, auditContext);
+      executionDelegated = true;
+      return task;
+    } finally {
+      if (!executionDelegated) {
+        auditContext.log(null);
+      }
     }
-    return new AuthorizerTask(statement);
   }
 
   private IConfigTask visitUpdateUser(AuthorStatement statement, 
MPPQueryContext context) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/relational/RelationalAuthorizerTask.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/relational/RelationalAuthorizerTask.java
index 6d74bbf038e..68541badde7 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/relational/RelationalAuthorizerTask.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/metadata/relational/RelationalAuthorizerTask.java
@@ -18,6 +18,7 @@
  */
 package 
org.apache.iotdb.db.queryengine.plan.execution.config.metadata.relational;
 
+import org.apache.iotdb.db.audit.UserRoleModificationAuditContext;
 import org.apache.iotdb.db.auth.AuthorityChecker;
 import org.apache.iotdb.db.queryengine.plan.analyze.QueryType;
 import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult;
@@ -29,16 +30,20 @@ import com.google.common.util.concurrent.ListenableFuture;
 
 public class RelationalAuthorizerTask implements IConfigTask {
   private final RelationalAuthorStatement statement;
+  private final UserRoleModificationAuditContext userRoleAuditContext;
 
-  public RelationalAuthorizerTask(RelationalAuthorStatement statement) {
+  public RelationalAuthorizerTask(
+      RelationalAuthorStatement statement, UserRoleModificationAuditContext 
userRoleAuditContext) {
     this.statement = statement;
+    this.userRoleAuditContext = userRoleAuditContext;
   }
 
   @Override
   public ListenableFuture<ConfigTaskResult> execute(IConfigTaskExecutor 
configTaskExecutor) {
     if (statement.getQueryType() != QueryType.READ
         && statement.getQueryType() != QueryType.READ_WRITE) {
-      return AuthorityChecker.operatePermission(statement);
+      return userRoleAuditContext.executeAndAudit(
+          () -> AuthorityChecker.operatePermission(statement));
     } else {
       return AuthorityChecker.queryPermission(statement);
     }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/AuthorizerTask.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/AuthorizerTask.java
index 3501497ddb3..4102e37ed1e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/AuthorizerTask.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/sys/AuthorizerTask.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.queryengine.plan.execution.config.sys;
 
+import org.apache.iotdb.db.audit.UserRoleModificationAuditContext;
 import org.apache.iotdb.db.auth.AuthorityChecker;
 import org.apache.iotdb.db.queryengine.plan.analyze.QueryType;
 import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult;
@@ -31,9 +32,16 @@ import com.google.common.util.concurrent.ListenableFuture;
 public class AuthorizerTask implements IConfigTask {
 
   private final AuthorStatement authorStatement;
+  private final UserRoleModificationAuditContext userRoleAuditContext;
 
   public AuthorizerTask(AuthorStatement authorStatement) {
+    this(authorStatement, UserRoleModificationAuditContext.empty());
+  }
+
+  public AuthorizerTask(
+      AuthorStatement authorStatement, UserRoleModificationAuditContext 
userRoleAuditContext) {
     this.authorStatement = authorStatement;
+    this.userRoleAuditContext = userRoleAuditContext;
   }
 
   @Override
@@ -42,7 +50,8 @@ public class AuthorizerTask implements IConfigTask {
     // If your operation is async, you can return the corresponding future 
directly.
     if (authorStatement.getQueryType() != QueryType.READ
         && authorStatement.getQueryType() != QueryType.READ_WRITE) {
-      return AuthorityChecker.operatePermission(authorStatement);
+      return userRoleAuditContext.executeAndAudit(
+          () -> AuthorityChecker.operatePermission(authorStatement));
     } else {
       return AuthorityChecker.queryPermission(authorStatement);
     }
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 73d8518a776..0d43f76fd29 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
@@ -645,10 +645,16 @@ public class TreeAccessCheckVisitor extends 
StatementVisitor<TSStatus, TreeAcces
             authorType == AuthorType.CREATE_ROLE || authorType == 
AuthorType.DROP_ROLE
                 ? statement::getRoleName
                 : () -> "user: " + statement.getUserName() + ", role: " + 
statement.getRoleName();
-        return checkGlobalAuth(
-            context.setAuditLogOperation(AuditLogOperation.DDL),
-            PrivilegeType.MANAGE_ROLE,
-            auditObject);
+        TSStatus status =
+            checkGlobalAuth(
+                context.setAuditLogOperation(AuditLogOperation.DDL),
+                PrivilegeType.MANAGE_ROLE,
+                auditObject);
+        if (authorType == AuthorType.GRANT_USER_ROLE || authorType == 
AuthorType.REVOKE_USER_ROLE) {
+          DNAuditLogger.getInstance()
+              .logUserRoleModificationAuthorizationFailure(statement, context, 
status);
+        }
+        return status;
 
       case REVOKE_USER:
       case GRANT_USER:
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerUserRoleModificationTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerUserRoleModificationTest.java
new file mode 100644
index 00000000000..57e8d327017
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerUserRoleModificationTest.java
@@ -0,0 +1,354 @@
+/*
+ * 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.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.exception.IoTDBException;
+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.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.function.Supplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+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.CALLS_REAL_METHODS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+public class DNAuditLoggerUserRoleModificationTest {
+
+  @Test
+  public void testTreeGrantRoleSuccess() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+    String sql = "GRANT ROLE role1 TO user1";
+
+    auditLogger.logUserRoleModification(
+        treeStatement(AuthorType.GRANT_USER_ROLE), sessionInfo(), sql, 
RpcUtils.SUCCESS_STATUS);
+
+    assertAuditLog(auditLogger, true, sql);
+  }
+
+  @Test
+  public void testTreeRevokeRoleFailure() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+    String sql = "REVOKE ROLE role1 FROM user1";
+
+    auditLogger.logUserRoleModification(
+        treeStatement(AuthorType.REVOKE_USER_ROLE),
+        sessionInfo(),
+        sql,
+        RpcUtils.getStatus(TSStatusCode.USER_NOT_HAS_ROLE));
+
+    assertAuditLog(auditLogger, false, sql);
+  }
+
+  @Test
+  public void testTableGrantRoleFailure() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+    String sql = "GRANT ROLE role1 TO user1";
+
+    auditLogger.logUserRoleModification(
+        tableStatement(AuthorRType.GRANT_USER_ROLE),
+        sessionInfo(),
+        sql,
+        RpcUtils.getStatus(TSStatusCode.ROLE_NOT_EXIST));
+
+    assertAuditLog(auditLogger, false, sql);
+  }
+
+  @Test
+  public void testTableRevokeRoleSuccess() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+    String sql = "REVOKE ROLE role1 FROM user1";
+
+    auditLogger.logUserRoleModification(
+        tableStatement(AuthorRType.REVOKE_USER_ROLE), sessionInfo(), sql, 
RpcUtils.SUCCESS_STATUS);
+
+    assertAuditLog(auditLogger, true, sql);
+  }
+
+  @Test
+  public void testTreeAuthorizationFailure() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+    String sql = "GRANT ROLE role1 TO user1";
+
+    auditLogger.logUserRoleModificationAuthorizationFailure(
+        treeStatement(AuthorType.GRANT_USER_ROLE),
+        treeAuditEntity(sql),
+        RpcUtils.getStatus(TSStatusCode.NO_PERMISSION));
+
+    assertAuditLog(auditLogger, false, sql);
+  }
+
+  @Test
+  public void testSuccessfulAuthorizationIsLoggedAfterExecutionOnly() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+
+    auditLogger.logUserRoleModificationAuthorizationFailure(
+        treeStatement(AuthorType.GRANT_USER_ROLE),
+        treeAuditEntity("grant role"),
+        RpcUtils.SUCCESS_STATUS);
+
+    verify(auditLogger, never()).log(any(), any());
+  }
+
+  @Test
+  public void testRedirectIsIgnored() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+
+    auditLogger.logUserRoleModification(
+        treeStatement(AuthorType.GRANT_USER_ROLE),
+        sessionInfo(),
+        "grant role",
+        RpcUtils.getStatus(TSStatusCode.REDIRECTION_RECOMMEND));
+
+    verify(auditLogger, never()).log(any(), any());
+  }
+
+  @Test
+  public void testPrivilegeGrantIsIgnored() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+    AuthorStatement statement = new AuthorStatement(AuthorType.GRANT_USER);
+    statement.setUserName("user1");
+
+    auditLogger.logUserRoleModification(statement, sessionInfo(), "grant 
privilege", null);
+
+    verify(auditLogger, never()).log(any(), any());
+  }
+
+  @Test
+  public void testMissingSessionIsIgnored() {
+    DNAuditLogger auditLogger = mock(DNAuditLogger.class, CALLS_REAL_METHODS);
+
+    auditLogger.logUserRoleModification(
+        treeStatement(AuthorType.GRANT_USER_ROLE), null, "grant role", null);
+
+    verify(auditLogger, never()).log(any(), any());
+  }
+
+  @Test
+  public void testConcreteTreeRoleOperationRecordsSuccess() {
+    UserRoleModificationAuditContext.AuditLogWriter auditLogWriter =
+        mock(UserRoleModificationAuditContext.AuditLogWriter.class);
+    UserRoleModificationAuditContext context =
+        UserRoleModificationAuditContext.forTreeStatement(
+            treeStatement(AuthorType.GRANT_USER_ROLE), sessionInfo(), 
auditLogWriter);
+    SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+
+    assertSame(future, context.executeAndAudit(() -> future));
+    future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS));
+
+    assertAuditStatus(auditLogWriter, TSStatusCode.SUCCESS_STATUS);
+  }
+
+  @Test
+  public void testConcreteTableRoleOperationRecordsFailure() {
+    UserRoleModificationAuditContext.AuditLogWriter auditLogWriter =
+        mock(UserRoleModificationAuditContext.AuditLogWriter.class);
+    UserRoleModificationAuditContext context =
+        UserRoleModificationAuditContext.forTableStatement(
+            tableStatement(AuthorRType.REVOKE_USER_ROLE), sessionInfo(), 
auditLogWriter);
+    SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+
+    context.executeAndAudit(() -> future);
+    future.set(new 
ConfigTaskResult(RpcUtils.getStatus(TSStatusCode.USER_NOT_HAS_ROLE)));
+
+    assertAuditStatus(auditLogWriter, TSStatusCode.USER_NOT_HAS_ROLE);
+  }
+
+  @Test
+  public void testConcreteRoleOperationRecordsUnexpectedFailure() {
+    UserRoleModificationAuditContext.AuditLogWriter auditLogWriter =
+        mock(UserRoleModificationAuditContext.AuditLogWriter.class);
+    UserRoleModificationAuditContext context =
+        UserRoleModificationAuditContext.forTreeStatement(
+            treeStatement(AuthorType.GRANT_USER_ROLE), sessionInfo(), 
auditLogWriter);
+    SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+
+    context.executeAndAudit(() -> future);
+    future.setException(new RuntimeException());
+
+    verify(auditLogWriter).log(null);
+  }
+
+  @Test
+  public void testConcreteRoleOperationIgnoresRedirectResult() {
+    UserRoleModificationAuditContext.AuditLogWriter auditLogWriter =
+        mock(UserRoleModificationAuditContext.AuditLogWriter.class);
+    UserRoleModificationAuditContext context =
+        UserRoleModificationAuditContext.forTreeStatement(
+            treeStatement(AuthorType.GRANT_USER_ROLE), sessionInfo(), 
auditLogWriter);
+    SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+
+    context.executeAndAudit(() -> future);
+    future.set(new ConfigTaskResult(TSStatusCode.REDIRECTION_RECOMMEND));
+
+    verify(auditLogWriter, never()).log(any());
+  }
+
+  @Test
+  public void testConcreteRoleOperationIgnoresRedirectException() {
+    UserRoleModificationAuditContext.AuditLogWriter auditLogWriter =
+        mock(UserRoleModificationAuditContext.AuditLogWriter.class);
+    UserRoleModificationAuditContext context =
+        UserRoleModificationAuditContext.forTreeStatement(
+            treeStatement(AuthorType.GRANT_USER_ROLE), sessionInfo(), 
auditLogWriter);
+    SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+
+    context.executeAndAudit(() -> future);
+    future.setException(new 
IoTDBException(RpcUtils.getStatus(TSStatusCode.REDIRECTION_RECOMMEND)));
+
+    verify(auditLogWriter, never()).log(any());
+  }
+
+  @Test
+  public void testConcreteRoleOperationRecordsSynchronousFailure() {
+    UserRoleModificationAuditContext.AuditLogWriter auditLogWriter =
+        mock(UserRoleModificationAuditContext.AuditLogWriter.class);
+    UserRoleModificationAuditContext context =
+        UserRoleModificationAuditContext.forTreeStatement(
+            treeStatement(AuthorType.GRANT_USER_ROLE), sessionInfo(), 
auditLogWriter);
+    RuntimeException exception = new RuntimeException();
+
+    RuntimeException actualException =
+        assertThrows(
+            RuntimeException.class,
+            () ->
+                context.executeAndAudit(
+                    () -> {
+                      throw exception;
+                    }));
+
+    assertSame(exception, actualException);
+    verify(auditLogWriter).log(null);
+  }
+
+  @Test
+  public void testUserRoleAuditContextRecordsOnlyOnce() {
+    UserRoleModificationAuditContext.AuditLogWriter auditLogWriter =
+        mock(UserRoleModificationAuditContext.AuditLogWriter.class);
+    UserRoleModificationAuditContext context =
+        UserRoleModificationAuditContext.forTreeStatement(
+            treeStatement(AuthorType.GRANT_USER_ROLE), sessionInfo(), 
auditLogWriter);
+
+    context.log(RpcUtils.SUCCESS_STATUS);
+    context.log(RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR));
+
+    assertAuditStatus(auditLogWriter, TSStatusCode.SUCCESS_STATUS);
+  }
+
+  @Test
+  public void testPrivilegeGrantDoesNotAuditConcreteOperation() {
+    UserRoleModificationAuditContext.AuditLogWriter auditLogWriter =
+        mock(UserRoleModificationAuditContext.AuditLogWriter.class);
+    UserRoleModificationAuditContext context =
+        UserRoleModificationAuditContext.forTreeStatement(
+            treeStatement(AuthorType.GRANT_USER), sessionInfo(), 
auditLogWriter);
+    SettableFuture<ConfigTaskResult> future = SettableFuture.create();
+
+    assertSame(future, context.executeAndAudit(() -> future));
+    future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS));
+
+    verify(auditLogWriter, never()).log(any());
+  }
+
+  private static AuthorStatement treeStatement(AuthorType type) {
+    AuthorStatement statement = new AuthorStatement(type);
+    statement.setUserName("user1");
+    statement.setRoleName("role1");
+    return statement;
+  }
+
+  private static RelationalAuthorStatement tableStatement(AuthorRType type) {
+    RelationalAuthorStatement statement = new RelationalAuthorStatement(type);
+    statement.setUserName("user1");
+    statement.setRoleName("role1");
+    return statement;
+  }
+
+  private static IAuditEntity treeAuditEntity(String sql) {
+    return new TreeAccessCheckContext(7L, "operator", "127.0.0.1")
+        .setDatabase("database")
+        .setSqlString(sql);
+  }
+
+  private static SessionInfo sessionInfo() {
+    return new SessionInfo(
+        1L,
+        new UserEntity(7L, "operator", "127.0.0.1"),
+        ZoneId.systemDefault(),
+        "database",
+        SqlDialect.TABLE);
+  }
+
+  private static void assertAuditStatus(
+      UserRoleModificationAuditContext.AuditLogWriter auditLogWriter,
+      TSStatusCode expectedStatusCode) {
+    ArgumentCaptor<TSStatus> statusCaptor = 
ArgumentCaptor.forClass(TSStatus.class);
+    verify(auditLogWriter).log(statusCaptor.capture());
+    assertEquals(expectedStatusCode.getStatusCode(), 
statusCaptor.getValue().getCode());
+  }
+
+  @SuppressWarnings("unchecked")
+  private static void assertAuditLog(DNAuditLogger auditLogger, boolean 
result, String sql) {
+    ArgumentCaptor<IAuditEntity> entityCaptor = 
ArgumentCaptor.forClass(IAuditEntity.class);
+    ArgumentCaptor<Supplier<String>> logCaptor = 
ArgumentCaptor.forClass(Supplier.class);
+    verify(auditLogger).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_ROLE_MEMBERSHIP, 
entity.getAuditEventType());
+    assertEquals(AuditLogOperation.CONTROL, entity.getAuditLogOperation());
+    assertEquals(PrivilegeType.SECURITY, entity.getPrivilegeTypes().get(0));
+    if (result) {
+      assertTrue(entity.getResult());
+    } else {
+      assertFalse(entity.getResult());
+    }
+    assertEquals("database", entity.getDatabase());
+    assertEquals(sql, entity.getSqlString());
+    assertEquals("user: user1, role: role1", logCaptor.getValue().get());
+  }
+}
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/audit/AuditEventType.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/audit/AuditEventType.java
index 510912a302f..58e6f752b60 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/audit/AuditEventType.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/audit/AuditEventType.java
@@ -37,6 +37,7 @@ public enum AuditEventType {
   LOGIN_FINAL,
   MODIFY_SECURITY_OPTIONS,
   MODIFY_DEFAULT_SECURITY_VALUES,
+  MODIFY_ROLE_MEMBERSHIP,
   REVOKE_FAILED,
   GRANT_ROLE_FAILED,
   LOGIN_RESOURCE_RESTRICT,

Reply via email to