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..26bd141f8b05c 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,30 @@ public void logRevokeFailure( status); } + public void logUserRoleModificationAuthorizationFailure( + Statement statement, IAuditEntity auditEntity, @Nullable TSStatus status) { + if (isSuccessful(status)) { + 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 +297,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_USER_ROLE, + 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 +398,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/auth/AuthorityChecker.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/auth/AuthorityChecker.java index fe8748ba4c335..6f79d1242dee9 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/auth/AuthorityChecker.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/auth/AuthorityChecker.java @@ -214,7 +214,9 @@ public static TSStatus checkAuthority(Statement statement, IAuditEntity auditEnt return status; } finally { PERFORMANCE_OVERVIEW_METRICS.recordAuthCost(System.nanoTime() - startTime); - DNAuditLogger.getInstance().logRevokeFailure(statement, auditEntity, status); + DNAuditLogger auditLogger = DNAuditLogger.getInstance(); + auditLogger.logRevokeFailure(statement, auditEntity, status); + auditLogger.logUserRoleModificationAuthorizationFailure(statement, auditEntity, status); } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/Coordinator.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/Coordinator.java index 8919182c7ce93..ed89a6dc163ca 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/Coordinator.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/Coordinator.java @@ -20,6 +20,7 @@ package org.apache.iotdb.db.queryengine.plan; import org.apache.iotdb.common.rpc.thrift.TEndPoint; +import org.apache.iotdb.common.rpc.thrift.TSStatus; import org.apache.iotdb.commons.client.ClientPoolFactory; import org.apache.iotdb.commons.client.IClientManager; import org.apache.iotdb.commons.client.async.AsyncDataNodeInternalServiceClient; @@ -413,8 +414,10 @@ public ExecutionResult executeForTreeModel( startTime))); return result; } finally { - DNAuditLogger.getInstance() - .logRevokeFailure(statement, session, sql, result == null ? null : result.status); + DNAuditLogger auditLogger = DNAuditLogger.getInstance(); + TSStatus status = result == null ? null : result.status; + auditLogger.logRevokeFailure(statement, session, sql, status); + auditLogger.logUserRoleModification(statement, session, sql, status); } } @@ -561,8 +564,10 @@ public ExecutionResult executeForTableModel( startTime))); return result; } finally { - DNAuditLogger.getInstance() - .logRevokeFailure(statement, session, sql, result == null ? null : result.status); + DNAuditLogger auditLogger = DNAuditLogger.getInstance(); + TSStatus status = result == null ? null : result.status; + auditLogger.logRevokeFailure(statement, session, sql, status); + auditLogger.logUserRoleModification(statement, session, sql, status); } } 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..4242635b26cea --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerUserRoleModificationTest.java @@ -0,0 +1,214 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.audit; + +import org.apache.iotdb.commons.audit.AuditEventType; +import org.apache.iotdb.commons.audit.AuditLogOperation; +import org.apache.iotdb.commons.audit.IAuditEntity; +import org.apache.iotdb.commons.audit.UserEntity; +import org.apache.iotdb.commons.auth.entity.PrivilegeType; +import org.apache.iotdb.commons.queryengine.common.SessionInfo; +import org.apache.iotdb.commons.queryengine.common.SqlDialect; +import org.apache.iotdb.db.queryengine.plan.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 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.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()); + } + + 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); + } + + @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_USER_ROLE, 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..4d8eb9c550e22 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_USER_ROLE, REVOKE_FAILED, GRANT_ROLE_FAILED, LOGIN_RESOURCE_RESTRICT,