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 b43037206ccdc..f3e8154dcde69 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 a319bce5000e6..5dd49c8804e7a 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 02fedbd5e1f19..96fe1b0446a73 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.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 void logRevokeFailure( 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 @@ private void logRevokeFailure( () -> 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 @@ private static String getTargetName( 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 0000000000000..ed7556b44cb0f --- /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 executeAndAudit( + Supplier> operation) { + try { + ListenableFuture future = operation.get(); + if (auditLogWriter == null) { + return future; + } + Futures.addCallback( + future, + new FutureCallback() { + @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 57a2bbd447922..5f0ff823972a0 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.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 IConfigTask visitShowCurrentTimestamp(ShowCurrentTimestamp node, MPPQuery @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 IConfigTask visitRelationalAuthorPlan( 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 573cf86b2cf99..d1e363f81e1c8 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.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 IConfigTask visitTestConnection( @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 6d74bbf038ea0..68541badde751 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 @@ 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 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 3501497ddb33b..4102e37ed1ead 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 @@ 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 ListenableFuture execute(IConfigTaskExecutor configTask // 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 73d8518a77638..0d43f76fd29ef 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 TSStatus visitAuthor(AuthorStatement statement, TreeAccessCheckContext co 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 0000000000000..57e8d327017d6 --- /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 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 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 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 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 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 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 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 entityCaptor = ArgumentCaptor.forClass(IAuditEntity.class); + ArgumentCaptor> 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 510912a302fdc..58e6f752b6096 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,