This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 4d40c75f0b7 branch-4.2: [fix](data security) Mask ai.api_key in
CREATE/ALTER RESOURCE logs #66006 (#70082)
4d40c75f0b7 is described below
commit 4d40c75f0b73a7096ca78525c6eeb8b4853a5785
Author: morrySnow <[email protected]>
AuthorDate: Wed Nov 4 08:23:04 2026 +0800
branch-4.2: [fix](data security) Mask ai.api_key in CREATE/ALTER RESOURCE
logs #66006 (#70082)
picked from master #66006
Co-authored-by: Wen Zhenghu <[email protected]>
---
.../java/org/apache/doris/catalog/ResourceMgr.java | 3 +-
.../parser/LogicalPlanBuilderForEncryption.java | 33 ++++--
.../java/org/apache/doris/qe/StmtExecutor.java | 72 +++++++++--
.../parser/RepositoryAuditEncryptionTest.java | 75 ++++++++++++
.../java/org/apache/doris/qe/StmtExecutorTest.java | 131 +++++++++++++++++++++
5 files changed, 297 insertions(+), 17 deletions(-)
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java
index f8ba286315b..2c7c16b78bc 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/ResourceMgr.java
@@ -159,7 +159,8 @@ public class ResourceMgr implements Writable {
// log alter
Env.getCurrentEnv().getEditLog().logAlterResource(resource);
- LOG.info("Alter resource success. Resource: {}", resource);
+ // Only log non-sensitive identifiers here because resource objects
may retain credential properties.
+ LOG.info("Alter resource success. Resource: {}, type: {}",
resource.getName(), resource.getType());
}
public void replayAlterResource(Resource resource) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java
index fea15eb812c..aee87a17c0c 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForEncryption.java
@@ -24,7 +24,6 @@ import org.apache.doris.common.util.PrintableMap;
import org.apache.doris.nereids.DorisParser;
import org.apache.doris.nereids.DorisParser.InsertTableContext;
import org.apache.doris.nereids.DorisParser.JobFromToClauseContext;
-import org.apache.doris.nereids.DorisParser.SupportedDmlStatementContext;
import org.apache.doris.nereids.trees.plans.commands.info.SetVarOp;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
@@ -179,6 +178,17 @@ public class LogicalPlanBuilderForEncryption extends
LogicalPlanBuilder {
return super.visitAlterAuthenticationIntegrationProperties(ctx);
}
+ @Override
+ public LogicalPlan visitAlterResource(DorisParser.AlterResourceContext
ctx) {
+ if (ctx.propertyClause() != null) {
+ DorisParser.PropertyClauseContext propertyClauseContext =
ctx.propertyClause();
+ encryptProperty(visitPropertyClause(propertyClauseContext),
+ propertyClauseContext.fileProperties.start.getStartIndex(),
+ propertyClauseContext.fileProperties.stop.getStopIndex());
+ }
+ return super.visitAlterResource(ctx);
+ }
+
// select from tvf
@Override
public LogicalPlan
visitTableValuedFunction(DorisParser.TableValuedFunctionContext ctx) {
@@ -193,9 +203,8 @@ public class LogicalPlanBuilderForEncryption extends
LogicalPlanBuilder {
// create job select tvf
@Override
public LogicalPlan
visitCreateScheduledJob(DorisParser.CreateScheduledJobContext ctx) {
- if (ctx.supportedDmlStatement() != null) {
- SupportedDmlStatementContext supportedDmlStatementContext =
ctx.supportedDmlStatement();
- visitInsertTable((InsertTableContext)
supportedDmlStatementContext);
+ if (ctx.supportedDmlStatement() instanceof InsertTableContext) {
+ visitInsertTable((InsertTableContext) ctx.supportedDmlStatement());
} else if (ctx.jobFromToClause() != null) {
JobFromToClauseContext jobFromToClauseContext =
ctx.jobFromToClause();
encryptProperty(visitPropertyItemList(jobFromToClauseContext.sourceProperties),
@@ -209,9 +218,8 @@ public class LogicalPlanBuilderForEncryption extends
LogicalPlanBuilder {
// alter job select tvf
@Override
public LogicalPlan visitAlterJob(DorisParser.AlterJobContext ctx) {
- SupportedDmlStatementContext supportedDmlStatementContext =
ctx.supportedDmlStatement();
- if (ctx.supportedDmlStatement() != null) {
- visitInsertTable((InsertTableContext)
supportedDmlStatementContext);
+ if (ctx.supportedDmlStatement() instanceof InsertTableContext) {
+ visitInsertTable((InsertTableContext) ctx.supportedDmlStatement());
} else if (ctx.jobFromToClause() != null) {
JobFromToClauseContext jobFromToClauseContext =
ctx.jobFromToClause();
encryptProperty(visitPropertyItemList(jobFromToClauseContext.sourceProperties),
@@ -222,6 +230,17 @@ public class LogicalPlanBuilderForEncryption extends
LogicalPlanBuilder {
return super.visitAlterJob(ctx);
}
+ @Override
+ public LogicalPlan visitCreateResource(DorisParser.CreateResourceContext
ctx) {
+ if (ctx.properties != null) {
+ DorisParser.PropertyClauseContext propertyClauseContext =
ctx.properties;
+ encryptProperty(visitPropertyClause(propertyClauseContext),
+ propertyClauseContext.fileProperties.start.getStartIndex(),
+ propertyClauseContext.fileProperties.stop.getStopIndex());
+ }
+ return super.visitCreateResource(ctx);
+ }
+
private void encryptProperty(Map<String, String> properties, int start,
int stop) {
if (MapUtils.isNotEmpty(properties)) {
PrintableMap<String, String> printableMap = new
PrintableMap<>(properties, "=",
diff --git a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
index 6c1ebaedc68..09068413e79 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/qe/StmtExecutor.java
@@ -95,6 +95,7 @@ import
org.apache.doris.nereids.trees.plans.commands.DeleteFromUsingCommand;
import org.apache.doris.nereids.trees.plans.commands.EmptyCommand;
import org.apache.doris.nereids.trees.plans.commands.Forward;
import org.apache.doris.nereids.trees.plans.commands.LoadCommand;
+import org.apache.doris.nereids.trees.plans.commands.NeedAuditEncryption;
import org.apache.doris.nereids.trees.plans.commands.PrepareCommand;
import org.apache.doris.nereids.trees.plans.commands.Redirect;
import org.apache.doris.nereids.trees.plans.commands.TransactionCommand;
@@ -175,6 +176,7 @@ public class StmtExecutor {
private static final Logger LOG = LogManager.getLogger(StmtExecutor.class);
private static final AtomicLong STMT_ID_GENERATOR = new AtomicLong(0);
+ private static final String MASKED_STMT_FALLBACK = "/* masked statement
unavailable */";
public static final int MAX_DATA_TO_SEND_FOR_TXN = 100;
private static Set<String> blockSqlAstNames = Sets.newHashSet();
@@ -521,7 +523,7 @@ public class StmtExecutor {
TUniqueId queryId = new TUniqueId(uuid.getMostSignificantBits(),
uuid.getLeastSignificantBits());
if (Config.enable_print_request_before_execution) {
LOG.info("begin to execute query {} {}",
- DebugUtil.printId(queryId), originStmt == null ? "null" :
originStmt.originStmt);
+ DebugUtil.printId(queryId),
getStmtForLoggingBeforeParse());
}
queryRetry(queryId);
}
@@ -697,7 +699,7 @@ public class StmtExecutor {
private void executeByNereids(TUniqueId queryId) throws Exception {
if (LOG.isDebugEnabled()) {
- LOG.debug("Nereids start to execute query:\n {}",
originStmt.originStmt);
+ LOG.debug("Nereids start to execute query:\n {}",
getStmtForLoggingBeforeParse());
}
context.setQueryId(queryId);
context.setStartTime();
@@ -778,15 +780,16 @@ public class StmtExecutor {
((Command) logicalPlan).run(context, this);
} catch (QueryStateException e) {
if (LOG.isDebugEnabled()) {
- LOG.debug("Command({}) process failed.",
originStmt.originStmt, e);
+ LOG.debug("Command({}) process failed.",
getStmtForLogging(originStmt.originStmt), e);
}
context.setState(e.getQueryState());
- throw new NereidsException("Command(" + originStmt.originStmt
+ ") process failed",
+ throw new NereidsException("Command(" +
getStmtForLogging(originStmt.originStmt)
+ + ") process failed",
new AnalysisException(e.getMessage(), e));
} catch (UserException e) {
// Return message to info client what happened.
if (LOG.isDebugEnabled()) {
- LOG.debug("Command({}) process failed.",
originStmt.originStmt, e);
+ LOG.debug("Command({}) process failed.",
getStmtForLogging(originStmt.originStmt), e);
}
if (Config.isCloudMode() &&
SystemInfoService.needRetryWithReplan(e.getDetailMessage())) {
// For errors in SystemInfoService.NEED_REPLAN_ERRORS,
@@ -794,15 +797,17 @@ public class StmtExecutor {
throw e;
}
context.getState().setError(e.getMysqlErrorCode(),
e.getMessage());
- throw new NereidsException("Command (" + originStmt.originStmt
+ ") process failed",
+ throw new NereidsException("Command (" +
getStmtForLogging(originStmt.originStmt)
+ + ") process failed",
new AnalysisException(e.getMessage(), e));
} catch (Exception | Error e) {
// Maybe our bug
if (LOG.isDebugEnabled()) {
- LOG.debug("Command({}) process failed.",
originStmt.originStmt, e);
+ LOG.debug("Command({}) process failed.",
getStmtForLogging(originStmt.originStmt), e);
}
context.getState().setError(ErrorCode.ERR_UNKNOWN_ERROR,
e.getMessage());
- throw new NereidsException("Command (" + originStmt.originStmt
+ ") process failed.",
+ throw new NereidsException("Command (" +
getStmtForLogging(originStmt.originStmt)
+ + ") process failed.",
new AnalysisException(e.getMessage() == null ?
e.toString() : e.getMessage(), e));
}
} else {
@@ -842,7 +847,7 @@ public class StmtExecutor {
planner.plan(parsedStmt,
context.getSessionVariable().toThrift());
checkBlockRulesByScan(planner);
} catch (Exception e) {
- LOG.warn("Nereids plan query failed:\n{}",
originStmt.originStmt, e);
+ LOG.warn("Nereids plan query failed:\n{}",
getStmtForLogging(originStmt.originStmt), e);
throw new NereidsException(new
AnalysisException(e.getMessage(), e));
}
profile.getSummaryProfile().setQueryPlanFinishTime(TimeUtils.getStartTimeMs());
@@ -2400,6 +2405,55 @@ public class StmtExecutor {
return "";
}
+ private String getStmtForLogging(String stmt) {
+ if (stmt == null) {
+ return stmt;
+ }
+ if (!(parsedStmt instanceof LogicalPlanAdapter)) {
+ return getStmtForLoggingBeforeParse(stmt);
+ }
+ // Internal export outfile tasks use an empty origin SQL, so audit
masking must skip reparsing here.
+ if (stmt.isEmpty()) {
+ return stmt;
+ }
+ LogicalPlan logicalPlan = ((LogicalPlanAdapter)
parsedStmt).getLogicalPlan();
+ if (!(logicalPlan instanceof NeedAuditEncryption)) {
+ return stmt;
+ }
+ try {
+ return ((NeedAuditEncryption) logicalPlan).geneEncryptionSQL(stmt);
+ } catch (Exception e) {
+ // Logging must not leak plaintext or change command behavior when
masking fails.
+ LOG.warn("failed to mask statement for FE logging", e);
+ return MASKED_STMT_FALLBACK;
+ }
+ }
+
+ private String getStmtForLoggingBeforeParse() {
+ return getStmtForLoggingBeforeParse(originStmt == null ? null :
originStmt.originStmt);
+ }
+
+ private String getStmtForLoggingBeforeParse(String stmt) {
+ if (stmt == null) {
+ return null;
+ }
+ // Empty SQL cannot produce a valid parse tree for audit masking, so
keep the original text.
+ if (stmt.isEmpty()) {
+ return stmt;
+ }
+ try {
+ LogicalPlan logicalPlan = new NereidsParser().parseSingle(stmt);
+ if (!(logicalPlan instanceof NeedAuditEncryption)) {
+ return stmt;
+ }
+ return ((NeedAuditEncryption) logicalPlan).geneEncryptionSQL(stmt);
+ } catch (Exception e) {
+ // Logging must fail closed before parsing so secrets never fall
back to plaintext.
+ LOG.warn("failed to prepare masked statement for FE logging", e);
+ return MASKED_STMT_FALLBACK;
+ }
+ }
+
public List<ByteBuffer> getProxyQueryResultBufList() {
return ((ProxyMysqlChannel)
context.getMysqlChannel()).getProxyResultBufferList();
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/RepositoryAuditEncryptionTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/RepositoryAuditEncryptionTest.java
new file mode 100644
index 00000000000..1802b227a21
--- /dev/null
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/RepositoryAuditEncryptionTest.java
@@ -0,0 +1,75 @@
+// 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.doris.nereids.parser;
+
+import org.apache.doris.nereids.trees.plans.commands.NeedAuditEncryption;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that CREATE / ALTER RESOURCE statements have their credential
properties masked
+ * before they reach the audit log, via {@link
NeedAuditEncryption#geneEncryptionSQL(String)}.
+ */
+public class RepositoryAuditEncryptionTest {
+
+ private String encrypt(String sql) {
+ LogicalPlan plan = new NereidsParser().parseSingle(sql);
+ Assertions.assertTrue(plan instanceof NeedAuditEncryption,
+ "command should be NeedAuditEncryption: " +
plan.getClass().getName());
+ NeedAuditEncryption cmd = (NeedAuditEncryption) plan;
+ Assertions.assertTrue(cmd.needAuditEncryption());
+ return cmd.geneEncryptionSQL(sql);
+ }
+
+ @Test
+ public void testCreateResourceMasksAiApiKey() {
+ String sql = "CREATE EXTERNAL RESOURCE \"ai_resource\" PROPERTIES ("
+ + "\"type\" = \"ai\", "
+ + "\"ai.api_key\" = \"sk-test\", "
+ + "\"ai.endpoint\" = \"https://api.test\")";
+ String masked = encrypt(sql);
+ Assertions.assertFalse(masked.contains("sk-test"), masked);
+ Assertions.assertTrue(masked.contains("*XXX"), masked);
+ Assertions.assertTrue(masked.contains("https://api.test"), masked);
+ }
+
+ @Test
+ public void testAlterResourceMasksAiApiKey() {
+ String sql = "ALTER RESOURCE \"ai_resource\" PROPERTIES ("
+ + "\"ai.api_key\" = \"sk-test\", "
+ + "\"ai.endpoint\" = \"https://api.test\")";
+ String masked = encrypt(sql);
+ Assertions.assertFalse(masked.contains("sk-test"), masked);
+ Assertions.assertTrue(masked.contains("*XXX"), masked);
+ Assertions.assertTrue(masked.contains("https://api.test"), masked);
+ }
+
+ @Test
+ public void testCreateJobWithUpdateDoesNotThrowClassCastException() {
+ // CREATE JOB ... DO <update> parses the DML as UpdateContext, not
InsertTableContext.
+ // geneEncryptionSQL must not fail with ClassCastException on such
statements.
+ String sql = "CREATE JOB job1 ON SCHEDULE AT CURRENT_TIMESTAMP DO
UPDATE t SET type = 2 WHERE type = 1";
+ LogicalPlan plan = new NereidsParser().parseSingle(sql);
+ Assertions.assertTrue(plan instanceof NeedAuditEncryption,
+ "command should be NeedAuditEncryption: " +
plan.getClass().getName());
+ NeedAuditEncryption cmd = (NeedAuditEncryption) plan;
+ Assertions.assertDoesNotThrow(() -> cmd.geneEncryptionSQL(sql));
+ }
+}
diff --git a/fe/fe-core/src/test/java/org/apache/doris/qe/StmtExecutorTest.java
b/fe/fe-core/src/test/java/org/apache/doris/qe/StmtExecutorTest.java
index a728707c88c..feab54cc48a 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/qe/StmtExecutorTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/qe/StmtExecutorTest.java
@@ -18,13 +18,16 @@
package org.apache.doris.qe;
import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.Env;
import org.apache.doris.catalog.InternalSchemaInitializer;
import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.ResourceMgr;
import org.apache.doris.catalog.ScalarType;
import org.apache.doris.common.Config;
import org.apache.doris.common.FeConstants;
import org.apache.doris.mysql.MysqlChannel;
import org.apache.doris.mysql.MysqlSerializer;
+import org.apache.doris.mysql.authenticate.TestLogAppender;
import org.apache.doris.nereids.StatementContext;
import org.apache.doris.planner.PlanFragment;
import org.apache.doris.planner.Planner;
@@ -56,6 +59,8 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class StmtExecutorTest extends TestWithFeService {
+ private static final String AI_RESOURCE_LOG_SECRET = "sk-test-secret";
+ private static final String MASKED_STMT_FALLBACK = "/* masked statement
unavailable */";
@Override
protected void runBeforeAll() throws Exception {
@@ -661,4 +666,130 @@ public class StmtExecutorTest extends TestWithFeService {
StmtExecutor executor = new StmtExecutor(mockCtx, new
OriginStatement("", 1), false);
executor.sendBinaryResultRow(resultSet);
}
+
+ @Test
+ public void testEmptyOriginStmtSkipsAuditMaskingReparse() throws Exception
{
+ org.apache.doris.nereids.trees.plans.logical.LogicalPlan logicalPlan =
Mockito.mock(
+ org.apache.doris.nereids.trees.plans.logical.LogicalPlan.class,
+ Mockito.withSettings().extraInterfaces(
+
org.apache.doris.nereids.trees.plans.commands.NeedAuditEncryption.class));
+ Mockito.doThrow(new AssertionError("empty SQL should not trigger audit
masking reparse"))
+
.when((org.apache.doris.nereids.trees.plans.commands.NeedAuditEncryption)
logicalPlan)
+ .geneEncryptionSQL("");
+
+ org.apache.doris.analysis.StatementBase parsedStmt = new
org.apache.doris.nereids.glue.LogicalPlanAdapter(
+ logicalPlan, new org.apache.doris.nereids.StatementContext());
+ parsedStmt.setOrigStmt(new OriginStatement("", 0));
+ StmtExecutor executor = new StmtExecutor(connectContext, parsedStmt);
+
+ // Empty internal SQL must bypass audit masking reparsing in both
logging paths.
+ Method getStmtForLogging =
StmtExecutor.class.getDeclaredMethod("getStmtForLogging", String.class);
+ getStmtForLogging.setAccessible(true);
+ Assertions.assertEquals("", getStmtForLogging.invoke(executor, ""));
+
+ Method getStmtForLoggingBeforeParse =
StmtExecutor.class.getDeclaredMethod("getStmtForLoggingBeforeParse");
+ getStmtForLoggingBeforeParse.setAccessible(true);
+ Assertions.assertEquals("",
getStmtForLoggingBeforeParse.invoke(executor));
+ }
+
+ @Test
+ public void testNeedAuditEncryptionStatementLogsMaskedSql() throws
Exception {
+ String resourceName = newAiResourceName();
+ boolean originalPrintRequest =
Config.enable_print_request_before_execution;
+ Config.enable_print_request_before_execution = true;
+ try (TestLogAppender appender =
TestLogAppender.attach(StmtExecutor.class)) {
+ connectContext.getState().reset();
+ StmtExecutor stmtExecutor = new StmtExecutor(connectContext,
buildCreateAiResourceSql(resourceName,
+ AI_RESOURCE_LOG_SECRET));
+ stmtExecutor.execute();
+
+
Assertions.assertFalse(appender.contains(org.apache.logging.log4j.Level.INFO,
AI_RESOURCE_LOG_SECRET));
+
Assertions.assertTrue(appender.contains(org.apache.logging.log4j.Level.INFO,
"*XXX"));
+
Assertions.assertFalse(appender.contains(org.apache.logging.log4j.Level.DEBUG,
AI_RESOURCE_LOG_SECRET));
+
Assertions.assertTrue(appender.contains(org.apache.logging.log4j.Level.DEBUG,
"*XXX"));
+ } finally {
+ Config.enable_print_request_before_execution =
originalPrintRequest;
+ }
+ connectContext.getState().reset();
+ StmtExecutor showExecutor = new StmtExecutor(connectContext, "");
+ showExecutor.execute();
+ Assertions.assertEquals(QueryState.MysqlStateType.OK,
connectContext.getState().getStateType());
+ }
+
+ @Test
+ public void testAlterResourceSuccessLogDoesNotPrintResourceObject() throws
Exception {
+ String resourceName = newAiResourceName();
+ createResource(buildCreateAiResourceSql(resourceName,
AI_RESOURCE_LOG_SECRET));
+ String alterSql = "ALTER RESOURCE \"" + resourceName + "\" PROPERTIES
("
+ + "\"ai.api_key\" = \"sk-updated-secret\")";
+ String fullResourceJson =
Env.getCurrentEnv().getResourceMgr().getResource(resourceName).toString();
+
+ try (TestLogAppender appender =
TestLogAppender.attach(ResourceMgr.class)) {
+ connectContext.getState().reset();
+ StmtExecutor stmtExecutor = new StmtExecutor(connectContext,
alterSql);
+ stmtExecutor.execute();
+
+
Assertions.assertFalse(appender.contains(org.apache.logging.log4j.Level.INFO,
"sk-updated-secret"));
+
Assertions.assertFalse(appender.contains(org.apache.logging.log4j.Level.INFO,
"\"properties\""));
+
Assertions.assertFalse(appender.contains(org.apache.logging.log4j.Level.INFO,
fullResourceJson));
+ }
+ }
+
+ @Test
+ public void testGetStmtForLoggingFailsClosedWhenMaskingThrows() throws
Exception {
+ org.apache.doris.nereids.trees.plans.logical.LogicalPlan logicalPlan =
Mockito.mock(
+ org.apache.doris.nereids.trees.plans.logical.LogicalPlan.class,
+ Mockito.withSettings().extraInterfaces(
+
org.apache.doris.nereids.trees.plans.commands.NeedAuditEncryption.class));
+ Mockito.doThrow(new IllegalStateException("masking failed"))
+
.when((org.apache.doris.nereids.trees.plans.commands.NeedAuditEncryption)
logicalPlan)
+ .geneEncryptionSQL(Mockito.anyString());
+
+ org.apache.doris.analysis.StatementBase parsedStmt = new
org.apache.doris.nereids.glue.LogicalPlanAdapter(
+ logicalPlan, new org.apache.doris.nereids.StatementContext());
+ parsedStmt.setOrigStmt(new OriginStatement("CREATE EXTERNAL RESOURCE
\"ai_resource\" PROPERTIES ("
+ + "\"ai.api_key\" = \"" + AI_RESOURCE_LOG_SECRET + "\")", 0));
+ StmtExecutor executor = new StmtExecutor(connectContext, parsedStmt);
+
+ Method getStmtForLogging =
StmtExecutor.class.getDeclaredMethod("getStmtForLogging", String.class);
+ getStmtForLogging.setAccessible(true);
+ Assertions.assertEquals(MASKED_STMT_FALLBACK,
getStmtForLogging.invoke(executor,
+ parsedStmt.getOrigStmt().originStmt));
+ }
+
+ @Test
+ public void testGetStmtForLoggingBeforeParseFailsClosedOnParseError()
throws Exception {
+ StmtExecutor executor = new StmtExecutor(connectContext,
+ "CREATE EXTERNAL RESOURCE \"broken_ai_resource\" PROPERTIES
(\"ai.api_key\" = \""
+ + AI_RESOURCE_LOG_SECRET + "\"");
+
+ Method getStmtForLoggingBeforeParse =
StmtExecutor.class.getDeclaredMethod("getStmtForLoggingBeforeParse");
+ getStmtForLoggingBeforeParse.setAccessible(true);
+ Assertions.assertEquals(MASKED_STMT_FALLBACK,
getStmtForLoggingBeforeParse.invoke(executor));
+ }
+
+ private void createResource(String sql) throws Exception {
+ connectContext.getState().reset();
+ StmtExecutor stmtExecutor = new StmtExecutor(connectContext, sql);
+ stmtExecutor.execute();
+ Assertions.assertEquals(QueryState.MysqlStateType.OK,
connectContext.getState().getStateType());
+ }
+
+ // Use unique resource names to keep log-masking tests isolated across the
PER_CLASS test fixture.
+ private static String newAiResourceName() {
+ return "ai_resource_log_test_" + System.nanoTime();
+ }
+
+ // Build resource SQL with a caller-provided name so tests do not share
catalog state.
+ private static String buildCreateAiResourceSql(String resourceName, String
apiKey) {
+ return "CREATE EXTERNAL RESOURCE \"" + resourceName + "\"\n"
+ + "PROPERTIES\n"
+ + "(\n"
+ + " \"type\" = \"ai\",\n"
+ + " \"ai.provider_type\" = \"openai\",\n"
+ + " \"ai.endpoint\" = \"https://api.test\",\n"
+ + " \"ai.model_name\" = \"gpt-test\",\n"
+ + " \"ai.api_key\" = \"" + apiKey + "\"\n"
+ + ");";
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]