diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java index 4cb29ccfbc844..e8e1954751bcb 100644 --- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java @@ -3799,5 +3799,7 @@ private DataNodeQueryMessages() {} public static final String EXCEPTION_ALIAS_IS_NULL_862D23B1 = "alias is null"; public static final String EXCEPTION_REFERENCES_IS_NULL_25A7E3AF = "references is null"; public static final String EXCEPTION_VISIBLEALIASES_IS_NULL_630B27F1 = "visibleAliases is null"; + public static final String EXCEPTION_HAS_NO_PERMISSION_TO_EXECUTE_ARG_BECAUSE_ONLY_THE_SUPERUSER_CAN_ALTER_HIM_HERSELF_C5902893 = + "Has no permission to execute %s, because only the superuser can alter him/herself."; } diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java index 849ce165efe31..c06b5332f7b2c 100644 --- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java @@ -4556,5 +4556,7 @@ private DataNodeQueryMessages() {} public static final String EXCEPTION_ALIAS_IS_NULL_862D23B1 = "alias 不能为空"; public static final String EXCEPTION_REFERENCES_IS_NULL_25A7E3AF = "references 不能为空"; public static final String EXCEPTION_VISIBLEALIASES_IS_NULL_630B27F1 = "visibleAliases 不能为空"; + public static final String EXCEPTION_HAS_NO_PERMISSION_TO_EXECUTE_ARG_BECAUSE_ONLY_THE_SUPERUSER_CAN_ALTER_HIM_HERSELF_C5902893 = + "无权执行 %s,因为只有超级用户可以修改其自身。"; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/DNAuditLogger.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/DNAuditLogger.java index 90026e37ba0fe..1f2b54d6d3421 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/DNAuditLogger.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/DNAuditLogger.java @@ -20,19 +20,41 @@ package org.apache.iotdb.db.audit; import org.apache.iotdb.commons.audit.AbstractAuditLogger; +import org.apache.iotdb.commons.audit.AuditEventType; import org.apache.iotdb.commons.audit.AuditLogFields; +import org.apache.iotdb.commons.audit.AuditLogOperation; import org.apache.iotdb.commons.audit.IAuditEntity; +import org.apache.iotdb.commons.audit.PrivilegeLevel; +import org.apache.iotdb.commons.auth.entity.PrivilegeType; import org.apache.iotdb.commons.exception.IllegalPathException; import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.commons.utils.CommonDateTimeUtils; import org.apache.iotdb.db.queryengine.plan.Coordinator; import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.utils.Binary; + import jakarta.validation.constraints.NotNull; +import java.util.Arrays; import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class DNAuditLogger extends AbstractAuditLogger { + // This text matcher is only a fallback. Password-update semantic nodes must clear sql_string + // before any audit entry is generated. + private static final Pattern ALTER_USER_PASSWORD_PATTERN = + Pattern.compile( + "^(\\s*ALTER\\s+USER\\s+.+?\\s+SET\\s+PASSWORD\\s+)" + + "(?:(?:U&)?'(?:''|[^'])*'|\"(?:\"\"|[^\"])*\")" + + "(\\s*;?\\s*)$", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private static final Pattern VALUES_PATTERN = Pattern.compile("(?i)(values)\\([^)]*\\)"); + private Coordinator coordinator; private DNAuditLogger() { @@ -50,7 +72,115 @@ public void setCoordinator(Coordinator coordinator) { @NotNull private static InsertRowStatement generateInsertStatement( IAuditEntity auditLogFields, String log, PartialPath logDevice) { - return null; + return generateInsertStatement( + auditLogFields, log, logDevice, CommonDateTimeUtils.currentTime()); + } + + @NotNull + static InsertRowStatement generateInsertStatement( + IAuditEntity auditLogFields, String log, PartialPath logDevice, long logTimestamp) { + String username = auditLogFields.getUsername(); + String address = auditLogFields.getCliHostname(); + AuditEventType type = auditLogFields.getAuditEventType(); + AuditLogOperation operation = auditLogFields.getAuditLogOperation(); + PrivilegeLevel privilegeLevel = PrivilegeLevel.GLOBAL; + if (auditLogFields.getPrivilegeTypes() != null) { + for (PrivilegeType privilegeType : auditLogFields.getPrivilegeTypes()) { + privilegeLevel = judgePrivilegeLevel(privilegeType); + if (privilegeLevel == PrivilegeLevel.GLOBAL) { + break; + } + } + } + + InsertRowStatement insertStatement = new InsertRowStatement(); + insertStatement.setDevicePath(logDevice); + insertStatement.setTime(logTimestamp); + insertStatement.setMeasurements( + new String[] { + AUDIT_LOG_USERNAME, + AUDIT_LOG_CLI_HOSTNAME, + AUDIT_LOG_AUDIT_EVENT_TYPE, + AUDIT_LOG_OPERATION_TYPE, + AUDIT_LOG_PRIVILEGE_TYPE, + AUDIT_LOG_PRIVILEGE_LEVEL, + AUDIT_LOG_RESULT, + AUDIT_LOG_DATABASE, + AUDIT_LOG_SQL_STRING, + AUDIT_LOG_LOG + }); + insertStatement.setAligned(true); + String sqlString = sanitizeAuditSql(auditLogFields.getSqlString()); + insertStatement.setValues( + new Object[] { + new Binary(username == null ? "null" : username, TSFileConfig.STRING_CHARSET), + new Binary(address == null ? "null" : address, TSFileConfig.STRING_CHARSET), + new Binary(type == null ? "null" : type.toString(), TSFileConfig.STRING_CHARSET), + new Binary( + operation == null ? "null" : operation.toString(), TSFileConfig.STRING_CHARSET), + new Binary( + auditLogFields.getPrivilegeTypes() == null + ? "null" + : auditLogFields.getPrivilegeTypeString(), + TSFileConfig.STRING_CHARSET), + new Binary(privilegeLevel.toString(), TSFileConfig.STRING_CHARSET), + auditLogFields.getResult(), + new Binary( + auditLogFields.getDatabase() == null ? "null" : auditLogFields.getDatabase(), + TSFileConfig.STRING_CHARSET), + new Binary(sqlString == null ? "null" : sqlString, TSFileConfig.STRING_CHARSET), + new Binary(log == null ? "null" : log, TSFileConfig.STRING_CHARSET) + }); + insertStatement.setDataTypes( + new TSDataType[] { + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.BOOLEAN, + TSDataType.STRING, + TSDataType.STRING, + TSDataType.STRING, + }); + return insertStatement; + } + + static String sanitizeAuditSql(String sqlString) { + if (sqlString == null) { + return null; + } + if (sqlString.regionMatches(true, 0, "CREATE USER", 0, "CREATE USER".length())) { + sqlString = String.join(" ", Arrays.asList(sqlString.split(" ")).subList(0, 3)) + " ..."; + } + Matcher alterUserMatcher = ALTER_USER_PASSWORD_PATTERN.matcher(sqlString); + if (alterUserMatcher.matches()) { + sqlString = alterUserMatcher.replaceFirst("$1...$2"); + } + return VALUES_PATTERN.matcher(sqlString).replaceAll("$1(...)"); + } + + private static PrivilegeLevel judgePrivilegeLevel(PrivilegeType type) { + if (type == null) { + return PrivilegeLevel.GLOBAL; + } + switch (type) { + case READ_DATA: + case DROP: + case ALTER: + case CREATE: + case DELETE: + case INSERT: + case SELECT: + case MANAGE_DATABASE: + case WRITE_DATA: + case READ_SCHEMA: + case WRITE_SCHEMA: + return PrivilegeLevel.OBJECT; + default: + return PrivilegeLevel.GLOBAL; + } } public void createViewIfNecessary() {} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditContext.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditContext.java new file mode 100644 index 0000000000000..d25064bf9cb4b --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditContext.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.audit; + +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.commons.audit.AuditEventType; +import org.apache.iotdb.commons.audit.AuditLogFields; +import org.apache.iotdb.commons.audit.AuditLogOperation; +import org.apache.iotdb.commons.audit.IAuditEntity; +import org.apache.iotdb.commons.auth.entity.PrivilegeType; +import org.apache.iotdb.commons.queryengine.common.SessionInfo; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RelationalAuthorStatement; +import org.apache.iotdb.db.queryengine.plan.relational.type.AuthorRType; +import org.apache.iotdb.db.queryengine.plan.statement.AuthorType; +import org.apache.iotdb.db.queryengine.plan.statement.Statement; +import org.apache.iotdb.db.queryengine.plan.statement.sys.AuthorStatement; +import org.apache.iotdb.rpc.TSStatusCode; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +/** Holds the security-safe audit fields for one password-change statement. */ +public final class PasswordChangeAuditContext { + + private static final PasswordChangeAuditContext EMPTY = + new PasswordChangeAuditContext(null, -1, null, null, null, null); + + private final AuditLogWriter auditLogWriter; + private final long userId; + private final String username; + private final String clientHostname; + private final String database; + private final String targetUsername; + private final AtomicBoolean logged = new AtomicBoolean(false); + + private PasswordChangeAuditContext( + AuditLogWriter auditLogWriter, + long userId, + String username, + String clientHostname, + String database, + String targetUsername) { + this.auditLogWriter = auditLogWriter; + this.userId = userId; + this.username = username; + this.clientHostname = clientHostname; + this.database = database; + this.targetUsername = targetUsername; + } + + public static PasswordChangeAuditContext forTreeStatement( + Statement statement, SessionInfo sessionInfo) { + String targetUsername = getTreeTargetUsername(statement); + return targetUsername == null + ? EMPTY + : fromSessionInfo(sessionInfo, targetUsername, DNAuditLogger.getInstance()::log); + } + + static PasswordChangeAuditContext forTreeStatement( + Statement statement, SessionInfo sessionInfo, AuditLogWriter auditLogWriter) { + String targetUsername = getTreeTargetUsername(statement); + return targetUsername == null + ? EMPTY + : fromSessionInfo(sessionInfo, targetUsername, auditLogWriter); + } + + public static PasswordChangeAuditContext forTableStatement( + org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement statement, + SessionInfo sessionInfo) { + String targetUsername = getTableTargetUsername(statement); + return targetUsername == null + ? EMPTY + : fromSessionInfo(sessionInfo, targetUsername, DNAuditLogger.getInstance()::log); + } + + static PasswordChangeAuditContext forTableStatement( + org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement statement, + SessionInfo sessionInfo, + AuditLogWriter auditLogWriter) { + String targetUsername = getTableTargetUsername(statement); + return targetUsername == null + ? EMPTY + : fromSessionInfo(sessionInfo, targetUsername, auditLogWriter); + } + + public static PasswordChangeAuditContext forTreeAuthorization( + Statement statement, IAuditEntity auditEntity) { + String targetUsername = getTreeTargetUsername(statement); + return targetUsername == null + ? EMPTY + : fromAuditEntity(auditEntity, targetUsername, DNAuditLogger.getInstance()::log); + } + + static PasswordChangeAuditContext forTreeAuthorization( + Statement statement, IAuditEntity auditEntity, AuditLogWriter auditLogWriter) { + String targetUsername = getTreeTargetUsername(statement); + return targetUsername == null + ? EMPTY + : fromAuditEntity(auditEntity, targetUsername, auditLogWriter); + } + + private static String getTreeTargetUsername(Statement statement) { + return statement instanceof AuthorStatement + && ((AuthorStatement) statement).getAuthorType() == AuthorType.UPDATE_USER + ? ((AuthorStatement) statement).getUserName() + : null; + } + + private static String getTableTargetUsername( + org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement statement) { + return statement instanceof RelationalAuthorStatement + && ((RelationalAuthorStatement) statement).getAuthorType() == AuthorRType.UPDATE_USER + ? ((RelationalAuthorStatement) statement).getUserName() + : null; + } + + private static PasswordChangeAuditContext fromAuditEntity( + IAuditEntity auditEntity, String targetUsername, AuditLogWriter auditLogWriter) { + return new PasswordChangeAuditContext( + auditLogWriter, + auditEntity.getUserId(), + auditEntity.getUsername(), + auditEntity.getCliHostname(), + auditEntity.getDatabase(), + targetUsername); + } + + private static PasswordChangeAuditContext fromSessionInfo( + SessionInfo sessionInfo, String targetUsername, AuditLogWriter auditLogWriter) { + return new PasswordChangeAuditContext( + auditLogWriter, + sessionInfo.getUserId(), + sessionInfo.getUserName(), + sessionInfo.getCliHostname(), + sessionInfo.getDatabaseName().orElse(null), + targetUsername); + } + + public void log(TSStatus status) { + if (targetUsername == null || !logged.compareAndSet(false, true)) { + return; + } + boolean result = + status != null + && (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode() + || status.getCode() == TSStatusCode.REDIRECTION_RECOMMEND.getStatusCode()); + auditLogWriter.log( + new AuditLogFields( + userId, + username, + clientHostname, + AuditEventType.MODIFY_PASSWD, + AuditLogOperation.CONTROL, + PrivilegeType.SECURITY, + result, + database, + null), + () -> targetUsername); + } + + boolean isEnabled() { + return targetUsername != null; + } + + @FunctionalInterface + interface AuditLogWriter { + + void log(IAuditEntity auditEntity, Supplier logMessage); + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditTask.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditTask.java new file mode 100644 index 0000000000000..f8080ee4a2595 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/audit/PasswordChangeAuditTask.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.audit; + +import org.apache.iotdb.common.rpc.thrift.TSStatus; +import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; +import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask; +import org.apache.iotdb.db.queryengine.plan.execution.config.executor.IConfigTaskExecutor; +import org.apache.iotdb.rpc.RpcUtils; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; + +import jakarta.validation.constraints.NotNull; + +/** Records the final result of a password-change config task without changing its execution. */ +public final class PasswordChangeAuditTask implements IConfigTask { + + private final IConfigTask delegate; + private final PasswordChangeAuditContext auditContext; + + private PasswordChangeAuditTask(IConfigTask delegate, PasswordChangeAuditContext auditContext) { + this.delegate = delegate; + this.auditContext = auditContext; + } + + public static IConfigTask wrap(IConfigTask delegate, PasswordChangeAuditContext auditContext) { + return auditContext.isEnabled() + ? new PasswordChangeAuditTask(delegate, auditContext) + : delegate; + } + + @Override + public ListenableFuture execute(IConfigTaskExecutor configTaskExecutor) + throws InterruptedException { + try { + final ListenableFuture future = delegate.execute(configTaskExecutor); + Futures.addCallback( + future, + new FutureCallback() { + @Override + public void onSuccess(ConfigTaskResult result) { + auditContext.log(toStatus(result)); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + auditContext.log(null); + } + }, + MoreExecutors.directExecutor()); + return future; + } catch (InterruptedException | RuntimeException | Error e) { + auditContext.log(null); + throw e; + } + } + + private static TSStatus toStatus(ConfigTaskResult result) { + if (result == null) { + return null; + } + if (result.getStatus() != null) { + return result.getStatus(); + } + return result.getStatusCode() == null ? null : RpcUtils.getStatus(result.getStatusCode()); + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java index d3926bcac79ce..495ebc42b65cf 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/common/MPPQueryContext.java @@ -80,6 +80,7 @@ public class MPPQueryContext implements IAuditEntity { private static final Logger LOGGER = LoggerFactory.getLogger(MPPQueryContext.class); private String sql; + private String auditSqlString; private final QueryId queryId; /** The type of explanation for a query. */ @@ -222,6 +223,7 @@ public MPPQueryContext( TEndPoint localInternalEndpoint) { this(queryId); this.sql = sql; + this.auditSqlString = sql; this.session = session; this.localQueryId = localQueryId; this.localDataBlockEndpoint = localDataBlockEndpoint; @@ -1040,12 +1042,12 @@ public IAuditEntity setDatabase(String database) { @Override public String getSqlString() { - return sql; + return auditSqlString; } @Override public IAuditEntity setSqlString(String sqlString) { - // Do nothing + this.auditSqlString = sqlString; return this; } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java index 820e4aa4075db..842d168c7483c 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TableConfigTaskVisitor.java @@ -52,6 +52,8 @@ import org.apache.iotdb.commons.schema.table.column.TsTableColumnSchema; import org.apache.iotdb.confignode.rpc.thrift.TDatabaseSchema; import org.apache.iotdb.db.audit.DNAuditLogger; +import org.apache.iotdb.db.audit.PasswordChangeAuditContext; +import org.apache.iotdb.db.audit.PasswordChangeAuditTask; import org.apache.iotdb.db.auth.AuthorityChecker; import org.apache.iotdb.db.conf.IoTDBConfig; import org.apache.iotdb.db.i18n.DataNodeQueryMessages; @@ -1626,18 +1628,30 @@ public IConfigTask visitShowCurrentTimestamp(ShowCurrentTimestamp node, MPPQuery @Override public IConfigTask visitRelationalAuthorPlan( RelationalAuthorStatement node, MPPQueryContext context) { - context.setQueryType(node.getQueryType()); - node.setExecutedByUserId(context.getUserId()); - TSStatus status = node.checkStatementIsValid(context.getSession().getUserName()); - if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { - throw new AccessDeniedException(status.getMessage()); - } - accessControl.checkUserCanRunRelationalAuthorStatement( - context.getSession().getUserName(), node, context); - if (node.getAuthorType() == AuthorRType.UPDATE_USER) { - visitUpdateUser(node); + PasswordChangeAuditContext auditContext = + PasswordChangeAuditContext.forTableStatement(node, context.getSession()); + boolean executionDelegated = false; + try { + context.setQueryType(node.getQueryType()); + node.setExecutedByUserId(context.getUserId()); + TSStatus status = node.checkStatementIsValid(context.getSession().getUserName()); + if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + throw new AccessDeniedException(status.getMessage()); + } + accessControl.checkUserCanRunRelationalAuthorStatement( + context.getSession().getUserName(), node, context); + if (node.getAuthorType() == AuthorRType.UPDATE_USER) { + visitUpdateUser(node); + } + IConfigTask task = + PasswordChangeAuditTask.wrap(new RelationalAuthorizerTask(node), auditContext); + executionDelegated = true; + return task; + } finally { + if (!executionDelegated) { + auditContext.log(null); + } } - return new RelationalAuthorizerTask(node); } private void visitUpdateUser(RelationalAuthorStatement node) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java index d0874e9cd219c..573cf86b2cf99 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/config/TreeConfigTaskVisitor.java @@ -27,6 +27,8 @@ import org.apache.iotdb.commons.executable.ExecutableManager; import org.apache.iotdb.commons.path.PartialPath; import org.apache.iotdb.commons.pipe.config.constant.SystemConstant; +import org.apache.iotdb.db.audit.PasswordChangeAuditContext; +import org.apache.iotdb.db.audit.PasswordChangeAuditTask; import org.apache.iotdb.db.auth.AuthorityChecker; import org.apache.iotdb.db.i18n.DataNodeQueryMessages; import org.apache.iotdb.db.queryengine.common.MPPQueryContext; @@ -339,7 +341,7 @@ public IConfigTask visitTestConnection( public IConfigTask visitAuthor(AuthorStatement statement, MPPQueryContext context) { statement.setExecutedByUserId(context.getUserId()); if (statement.getAuthorType() == AuthorType.UPDATE_USER) { - visitUpdateUser(statement); + return visitUpdateUser(statement, context); } if (statement.getAuthorType() == AuthorType.RENAME_USER) { visitRenameUser(statement); @@ -351,11 +353,27 @@ public IConfigTask visitAuthor(AuthorStatement statement, MPPQueryContext contex return new AuthorizerTask(statement); } - private void visitUpdateUser(AuthorStatement statement) { - statement.setPassWord( - AuthorityChecker.getAuthorityFetcher() - .getUser(statement.getUserName(), true) - .getPassword()); + private IConfigTask visitUpdateUser(AuthorStatement statement, MPPQueryContext context) { + PasswordChangeAuditContext auditContext = + PasswordChangeAuditContext.forTreeStatement(statement, context.getSession()); + boolean executionDelegated = false; + try { + statement.setPassWord( + AuthorityChecker.getAuthorityFetcher() + .getUser(statement.getUserName(), true) + .getPassword()); + TSStatus status = statement.checkStatementIsValid(context.getSession().getUserName()); + if (status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + throw new AccessDeniedException(status.getMessage()); + } + IConfigTask task = PasswordChangeAuditTask.wrap(new AuthorizerTask(statement), auditContext); + executionDelegated = true; + return task; + } finally { + if (!executionDelegated) { + auditContext.log(null); + } + } } private void visitRenameUser(AuthorStatement statement) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/AccessControlImpl.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/AccessControlImpl.java index 1a50f15e9cd49..8797d534a4587 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/AccessControlImpl.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/AccessControlImpl.java @@ -288,6 +288,9 @@ public void checkUserCanRunRelationalAuthorStatement( return; case RENAME_USER: case UPDATE_USER: + if (type == AuthorRType.UPDATE_USER) { + auditEntity.setSqlString(null); + } auditEntity.setAuditLogOperation(AuditLogOperation.DDL); if (statement.getUserName().equals(userName)) { // users can change the username and password of themselves diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java index 9d5a97710595b..73d8518a77638 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/security/TreeAccessCheckVisitor.java @@ -34,6 +34,7 @@ import org.apache.iotdb.commons.schema.table.Audit; import org.apache.iotdb.commons.utils.StatusUtils; import org.apache.iotdb.db.audit.DNAuditLogger; +import org.apache.iotdb.db.audit.PasswordChangeAuditContext; import org.apache.iotdb.db.auth.AuthorityChecker; import org.apache.iotdb.db.i18n.DataNodeQueryMessages; import org.apache.iotdb.db.queryengine.plan.statement.AuthorType; @@ -569,30 +570,9 @@ public TSStatus visitAuthor(AuthorStatement statement, TreeAccessCheckContext co PrivilegeType.MANAGE_USER, statement::getUserName); case UPDATE_USER: + return checkCanUpdateUser(statement, context); case RENAME_USER: - context.setAuditLogOperation(AuditLogOperation.DDL); - if (statement.getUserName().equals(context.getUsername())) { - // users can change the username and password of themselves - AUDIT_LOGGER.recordObjectAuthenticationAuditLog( - context.setResult(true), context::getUsername); - return RpcUtils.SUCCESS_STATUS; - } - if (AuthorityChecker.SUPER_USER_ID - == AuthorityChecker.getUserId(statement.getUserName()).orElse(-1L)) { - // Only the superuser can alter him/herself - AUDIT_LOGGER.recordObjectAuthenticationAuditLog( - context.setResult(false), context::getUsername); - return AuthorityChecker.getTSStatus( - false, - "Has no permission to execute " - + authorType - + ", because only the superuser can alter him/herself."); - } - context.setPrivilegeType(PrivilegeType.SECURITY); - return checkGlobalAuth( - context.setAuditLogOperation(AuditLogOperation.DDL), - PrivilegeType.MANAGE_USER, - statement::getUserName); + return checkCanAlterUser(statement, context); case LIST_USER: context.setAuditLogOperation(AuditLogOperation.QUERY).setResult(true); if (checkHasGlobalAuth( @@ -698,6 +678,48 @@ public TSStatus visitAuthor(AuthorStatement statement, TreeAccessCheckContext co } } + private TSStatus checkCanUpdateUser(AuthorStatement statement, TreeAccessCheckContext context) { + context.setSqlString(null); + PasswordChangeAuditContext auditContext = + PasswordChangeAuditContext.forTreeAuthorization(statement, context); + TSStatus status = null; + try { + status = checkCanAlterUser(statement, context); + return status; + } finally { + if (status == null || status.getCode() != TSStatusCode.SUCCESS_STATUS.getStatusCode()) { + auditContext.log(status); + } + } + } + + private TSStatus checkCanAlterUser(AuthorStatement statement, TreeAccessCheckContext context) { + context.setAuditLogOperation(AuditLogOperation.DDL); + if (statement.getUserName().equals(context.getUsername())) { + // users can change the username and password of themselves + AUDIT_LOGGER.recordObjectAuthenticationAuditLog( + context.setResult(true), context::getUsername); + return RpcUtils.SUCCESS_STATUS; + } + if (AuthorityChecker.SUPER_USER_ID + == AuthorityChecker.getUserId(statement.getUserName()).orElse(-1L)) { + // Only the superuser can alter him/herself + AUDIT_LOGGER.recordObjectAuthenticationAuditLog( + context.setResult(false), context::getUsername); + return AuthorityChecker.getTSStatus( + false, + String.format( + DataNodeQueryMessages + .EXCEPTION_HAS_NO_PERMISSION_TO_EXECUTE_ARG_BECAUSE_ONLY_THE_SUPERUSER_CAN_ALTER_HIM_HERSELF_C5902893, + statement.getAuthorType())); + } + context.setPrivilegeType(PrivilegeType.SECURITY); + return checkGlobalAuth( + context.setAuditLogOperation(AuditLogOperation.DDL), + PrivilegeType.MANAGE_USER, + statement::getUserName); + } + // =================================== CQ related ==================================== @Override public TSStatus visitCreateContinuousQuery( diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerTest.java new file mode 100644 index 0000000000000..26efc9ece3763 --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/DNAuditLoggerTest.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.audit; + +import org.apache.iotdb.common.rpc.thrift.TEndPoint; +import org.apache.iotdb.commons.audit.AuditEventType; +import org.apache.iotdb.commons.audit.AuditLogOperation; +import org.apache.iotdb.commons.audit.IAuditEntity; +import org.apache.iotdb.commons.audit.UserEntity; +import org.apache.iotdb.commons.auth.entity.PrivilegeType; +import org.apache.iotdb.commons.path.PartialPath; +import org.apache.iotdb.commons.queryengine.common.SessionInfo; +import org.apache.iotdb.commons.queryengine.common.SqlDialect; +import org.apache.iotdb.db.protocol.session.IClientSession; +import org.apache.iotdb.db.protocol.session.InternalClientSession; +import org.apache.iotdb.db.queryengine.common.MPPQueryContext; +import org.apache.iotdb.db.queryengine.common.QueryId; +import org.apache.iotdb.db.queryengine.plan.parser.StatementGenerator; +import org.apache.iotdb.db.queryengine.plan.relational.security.AccessControlImpl; +import org.apache.iotdb.db.queryengine.plan.relational.security.ITableAuthChecker; +import org.apache.iotdb.db.queryengine.plan.relational.security.TreeAccessCheckContext; +import org.apache.iotdb.db.queryengine.plan.relational.security.TreeAccessCheckVisitor; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RelationalAuthorStatement; +import org.apache.iotdb.db.queryengine.plan.relational.sql.parser.SqlParser; +import org.apache.iotdb.db.queryengine.plan.relational.type.AuthorRType; +import org.apache.iotdb.db.queryengine.plan.statement.Statement; +import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement; +import org.apache.iotdb.db.queryengine.plan.statement.sys.AuthorStatement; + +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.utils.Binary; +import org.junit.Test; + +import java.time.ZoneId; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +public class DNAuditLoggerTest { + + @Test + public void testAlterUserPasswordSqlIsSanitized() { + assertEquals( + "alter user alice set password ...", + DNAuditLogger.sanitizeAuditSql("alter user alice set password 'secret'")); + assertEquals( + "ALTER USER\talice\nSET PASSWORD\t...;", + DNAuditLogger.sanitizeAuditSql("ALTER USER\talice\nSET PASSWORD\t\"Secret@123456\";")); + assertEquals( + "ALTER USER alice SET PASSWORD ...;", + DNAuditLogger.sanitizeAuditSql("ALTER USER alice SET PASSWORD 'pass''word';")); + assertEquals( + "ALTER USER alice SET PASSWORD ...", + DNAuditLogger.sanitizeAuditSql("ALTER USER alice SET PASSWORD \"pass\"\"word\"")); + assertEquals( + "ALTER USER alice SET PASSWORD ...;", + DNAuditLogger.sanitizeAuditSql("ALTER USER alice SET PASSWORD U&'pass\\0077ord';")); + } + + @Test + public void testNonPasswordAlterUserSqlIsNotSanitized() { + assertSqlIsUnchanged("ALTER USER alice RENAME TO bob;"); + assertSqlIsUnchanged("ALTER USER alice ACCOUNT UNLOCK;"); + assertSqlIsUnchanged("ALTER USER alice SET MAX_SESSION_PER_USER 10;"); + assertSqlIsUnchanged("ALTER USER alice SET MIN_SESSION_PER_USER 1;"); + } + + @Test + public void testExistingAuditSqlSanitizationIsPreserved() { + assertNull(DNAuditLogger.sanitizeAuditSql(null)); + assertEquals( + "CREATE USER alice ...", + DNAuditLogger.sanitizeAuditSql("CREATE USER alice 'Secret@123456'")); + assertEquals( + "INSERT INTO root.sg.d(time, s) values(...)", + DNAuditLogger.sanitizeAuditSql("INSERT INTO root.sg.d(time, s) values(1, 'secret')")); + } + + @Test + public void testTreeObjectAuthenticationSqlIsSanitizedBeforePersistence() throws Exception { + String originalSql = "ALTER USER alice SET PASSWORD 'treeSecret';"; + Statement statement = StatementGenerator.createStatement(originalSql, ZoneId.systemDefault()); + assertTrue(statement instanceof AuthorStatement); + TreeAccessCheckContext auditEntity = + (TreeAccessCheckContext) + new TreeAccessCheckContext(7L, "alice", "127.0.0.1").setSqlString(originalSql); + + statement.accept(new TreeAccessCheckVisitor(), auditEntity); + + assertPersistedAuditSql(objectAuthenticationAuditEntity(auditEntity, true), "null"); + } + + @Test + public void testTableObjectAuthenticationSqlIsRemovedAfterParsingBeforePersistence() + throws Exception { + assertTablePasswordSqlIsRemoved("ALTER USER alice SET PASSWORD 'Secret' -- rotate", "Secret"); + assertTablePasswordSqlIsRemoved( + "ALTER USER alice SET PASSWORD 'Secret' /* rotate */", "Secret"); + assertTablePasswordSqlIsRemoved( + "ALTER USER alice SET PASSWORD U&'Secret' UESCAPE '#'", "Secret"); + } + + private static void assertTablePasswordSqlIsRemoved(String originalSql, String password) + throws Exception { + IClientSession clientSession = new InternalClientSession("table_audit_test"); + clientSession.setDatabaseName("database"); + org.apache.iotdb.commons.queryengine.plan.relational.sql.ast.Statement parsedStatement = + new SqlParser().createStatement(originalSql, ZoneId.systemDefault(), clientSession); + assertTrue(parsedStatement instanceof RelationalAuthorStatement); + RelationalAuthorStatement authorStatement = (RelationalAuthorStatement) parsedStatement; + assertEquals(AuthorRType.UPDATE_USER, authorStatement.getAuthorType()); + + MPPQueryContext queryContext = + new MPPQueryContext( + originalSql, + new QueryId("table_audit_test"), + new SessionInfo( + 1L, + new UserEntity(7L, "alice", "127.0.0.1"), + ZoneId.systemDefault(), + "database", + SqlDialect.TABLE), + new TEndPoint(), + new TEndPoint()); + new AccessControlImpl(mock(ITableAuthChecker.class), new TreeAccessCheckVisitor()) + .checkUserCanRunRelationalAuthorStatement("alice", authorStatement, queryContext); + + assertEquals(originalSql, queryContext.getSql()); + assertNull(queryContext.getSqlString()); + String persistedSql = getPersistedAuditSql(objectAuthenticationAuditEntity(queryContext, true)); + assertEquals("null", persistedSql); + assertFalse(persistedSql.contains(password)); + } + + private static void assertSqlIsUnchanged(String sql) { + assertEquals(sql, DNAuditLogger.sanitizeAuditSql(sql)); + } + + private static IAuditEntity objectAuthenticationAuditEntity( + IAuditEntity auditEntity, boolean result) { + return auditEntity + .setAuditEventType(AuditEventType.OBJECT_AUTHENTICATION) + .setAuditLogOperation(AuditLogOperation.DDL) + .setPrivilegeType(PrivilegeType.SECURITY) + .setResult(result) + .setDatabase("database"); + } + + private static void assertPersistedAuditSql(IAuditEntity auditEntity, String expectedSql) + throws Exception { + assertEquals(expectedSql, getPersistedAuditSql(auditEntity)); + } + + private static String getPersistedAuditSql(IAuditEntity auditEntity) throws Exception { + InsertRowStatement statement = + DNAuditLogger.generateInsertStatement( + auditEntity, "alice", new PartialPath("root.__audit.log.node_0.u_7"), 1L); + + return ((Binary) statement.getValues()[8]).getStringValue(TSFileConfig.STRING_CHARSET); + } +} diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/PasswordChangeAuditContextTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/PasswordChangeAuditContextTest.java new file mode 100644 index 0000000000000..ac340762e8c7b --- /dev/null +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/audit/PasswordChangeAuditContextTest.java @@ -0,0 +1,327 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.iotdb.db.audit; + +import org.apache.iotdb.commons.audit.AuditEventType; +import org.apache.iotdb.commons.audit.AuditLogOperation; +import org.apache.iotdb.commons.audit.IAuditEntity; +import org.apache.iotdb.commons.audit.UserEntity; +import org.apache.iotdb.commons.auth.entity.PrivilegeType; +import org.apache.iotdb.commons.queryengine.common.SessionInfo; +import org.apache.iotdb.commons.queryengine.common.SqlDialect; +import org.apache.iotdb.db.queryengine.plan.execution.config.ConfigTaskResult; +import org.apache.iotdb.db.queryengine.plan.execution.config.IConfigTask; +import org.apache.iotdb.db.queryengine.plan.relational.security.TreeAccessCheckContext; +import org.apache.iotdb.db.queryengine.plan.relational.sql.ast.RelationalAuthorStatement; +import org.apache.iotdb.db.queryengine.plan.relational.type.AuthorRType; +import org.apache.iotdb.db.queryengine.plan.statement.AuthorType; +import org.apache.iotdb.db.queryengine.plan.statement.sys.AuthorStatement; +import org.apache.iotdb.rpc.RpcUtils; +import org.apache.iotdb.rpc.TSStatusCode; + +import com.google.common.util.concurrent.SettableFuture; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.time.ZoneId; +import java.util.Collections; +import java.util.function.Supplier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class PasswordChangeAuditContextTest { + + @Test + public void testTreePasswordChangeSuccess() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + AuthorStatement statement = treePasswordChangeStatement(); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement(statement, sessionInfo(), auditLogWriter); + + context.log(RpcUtils.SUCCESS_STATUS); + + assertAuditLog(auditLogWriter, true); + } + + @Test + public void testTablePasswordChangeFailure() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + RelationalAuthorStatement statement = + new RelationalAuthorStatement( + AuthorRType.UPDATE_USER, + "target_user", + null, + null, + null, + Collections.emptySet(), + false, + null); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTableStatement( + statement, sessionInfo(SqlDialect.TABLE), auditLogWriter); + + context.log(RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR)); + + assertAuditLog(auditLogWriter, false); + } + + @Test + public void testOtherAuthorStatementIsIgnored() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement( + new AuthorStatement(AuthorType.CREATE_USER), sessionInfo(), auditLogWriter); + + context.log(RpcUtils.SUCCESS_STATUS); + + verify(auditLogWriter, never()).log(any(), any()); + } + + @Test + public void testRenameUserStatementIsIgnored() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeAuthorization( + new AuthorStatement(AuthorType.RENAME_USER), + new TreeAccessCheckContext(7L, "operator", "127.0.0.1"), + auditLogWriter); + + context.log(null); + + verify(auditLogWriter, never()).log(any(), any()); + } + + @Test + public void testDeniedTreePasswordChangeUsesAuthorizationIdentity() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + AuthorStatement statement = treePasswordChangeStatement(); + TreeAccessCheckContext authorizationContext = + new TreeAccessCheckContext(7L, "operator", "127.0.0.1"); + authorizationContext.setDatabase("database"); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeAuthorization( + statement, authorizationContext, auditLogWriter); + + context.log(null); + + assertAuditLog(auditLogWriter, false); + } + + @Test + public void testPasswordChangeAuditIsRecordedOnlyOnce() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement( + treePasswordChangeStatement(), sessionInfo(), auditLogWriter); + + context.log(RpcUtils.SUCCESS_STATUS); + context.log(RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR)); + + assertAuditLog(auditLogWriter, true); + } + + @Test + public void testPasswordChangeAuditTaskRecordsSuccess() throws Exception { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement( + treePasswordChangeStatement(), sessionInfo(), auditLogWriter); + SettableFuture future = SettableFuture.create(); + IConfigTask delegate = ignored -> future; + IConfigTask task = PasswordChangeAuditTask.wrap(delegate, context); + + assertSame(future, task.execute(null)); + future.set(new ConfigTaskResult(TSStatusCode.SUCCESS_STATUS)); + + assertAuditLog(auditLogWriter, true); + } + + @Test + public void testPasswordChangeAuditTaskRecordsAsynchronousFailure() throws Exception { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTableStatement( + tablePasswordChangeStatement(), sessionInfo(SqlDialect.TABLE), auditLogWriter); + SettableFuture future = SettableFuture.create(); + IConfigTask task = PasswordChangeAuditTask.wrap(ignored -> future, context); + + task.execute(null); + future.setException(new RuntimeException()); + + assertAuditLog(auditLogWriter, false); + } + + @Test + public void testPasswordChangeAuditTaskRecordsFailedResult() throws Exception { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement( + treePasswordChangeStatement(), sessionInfo(), auditLogWriter); + SettableFuture future = SettableFuture.create(); + IConfigTask task = PasswordChangeAuditTask.wrap(ignored -> future, context); + + task.execute(null); + future.set(new ConfigTaskResult(RpcUtils.getStatus(TSStatusCode.EXECUTE_STATEMENT_ERROR))); + + assertAuditLog(auditLogWriter, false); + } + + @Test + public void testPasswordChangeAuditTaskTreatsRedirectionAsSuccess() throws Exception { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement( + treePasswordChangeStatement(), sessionInfo(), auditLogWriter); + SettableFuture future = SettableFuture.create(); + IConfigTask task = PasswordChangeAuditTask.wrap(ignored -> future, context); + + task.execute(null); + future.set(new ConfigTaskResult(TSStatusCode.REDIRECTION_RECOMMEND)); + + assertAuditLog(auditLogWriter, true); + } + + @Test + public void testPasswordChangeAuditTaskRecordsSynchronousFailure() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement( + treePasswordChangeStatement(), sessionInfo(), auditLogWriter); + IConfigTask task = + PasswordChangeAuditTask.wrap( + ignored -> { + throw new IllegalStateException(); + }, + context); + + assertThrows(IllegalStateException.class, () -> task.execute(null)); + + assertAuditLog(auditLogWriter, false); + } + + @Test + public void testPasswordChangeAuditTaskRecordsInterruption() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement( + treePasswordChangeStatement(), sessionInfo(), auditLogWriter); + IConfigTask task = + PasswordChangeAuditTask.wrap( + ignored -> { + throw new InterruptedException(); + }, + context); + + assertThrows(InterruptedException.class, () -> task.execute(null)); + + assertAuditLog(auditLogWriter, false); + } + + @Test + public void testPasswordChangeAuditTaskDoesNotWrapOtherStatements() { + PasswordChangeAuditContext.AuditLogWriter auditLogWriter = + mock(PasswordChangeAuditContext.AuditLogWriter.class); + PasswordChangeAuditContext context = + PasswordChangeAuditContext.forTreeStatement( + new AuthorStatement(AuthorType.RENAME_USER), sessionInfo(), auditLogWriter); + IConfigTask delegate = ignored -> SettableFuture.create(); + + assertSame(delegate, PasswordChangeAuditTask.wrap(delegate, context)); + } + + private static SessionInfo sessionInfo() { + return sessionInfo(SqlDialect.TREE); + } + + private static SessionInfo sessionInfo(SqlDialect sqlDialect) { + return new SessionInfo( + 1L, + new UserEntity(7L, "operator", "127.0.0.1"), + ZoneId.systemDefault(), + "database", + sqlDialect); + } + + private static AuthorStatement treePasswordChangeStatement() { + return new AuthorStatement(AuthorType.UPDATE_USER) { + @Override + public String getUserName() { + return "target_user"; + } + }; + } + + private static RelationalAuthorStatement tablePasswordChangeStatement() { + return new RelationalAuthorStatement( + AuthorRType.UPDATE_USER, + "target_user", + null, + null, + null, + Collections.emptySet(), + false, + null); + } + + @SuppressWarnings("unchecked") + private static void assertAuditLog( + PasswordChangeAuditContext.AuditLogWriter auditLogWriter, boolean expectedResult) { + ArgumentCaptor entityCaptor = ArgumentCaptor.forClass(IAuditEntity.class); + ArgumentCaptor> logCaptor = ArgumentCaptor.forClass(Supplier.class); + verify(auditLogWriter).log(entityCaptor.capture(), logCaptor.capture()); + + IAuditEntity entity = entityCaptor.getValue(); + assertEquals(7L, entity.getUserId()); + assertEquals("operator", entity.getUsername()); + assertEquals("127.0.0.1", entity.getCliHostname()); + assertEquals(AuditEventType.MODIFY_PASSWD, entity.getAuditEventType()); + assertEquals(AuditLogOperation.CONTROL, entity.getAuditLogOperation()); + assertEquals(PrivilegeType.SECURITY, entity.getPrivilegeTypes().get(0)); + if (expectedResult) { + assertTrue(entity.getResult()); + } else { + assertFalse(entity.getResult()); + } + assertEquals("database", entity.getDatabase()); + assertNull(entity.getSqlString()); + assertEquals("target_user", logCaptor.getValue().get()); + } +}