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,