This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new a8928245ee5 [fix](data security) Mask ai.api_key in CREATE/ALTER
RESOURCE logs (#66006)
a8928245ee5 is described below
commit a8928245ee5712485d43f3dea45872ccb39e9437
Author: Wen Zhenghu <[email protected]>
AuthorDate: Tue Aug 11 09:37:21 2026 +0800
[fix](data security) Mask ai.api_key in CREATE/ALTER RESOURCE logs (#66006)
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
This PR is scoped only to fixing plaintext leakage of `ai.api_key` in
`CREATE RESOURCE` and `ALTER RESOURCE` statements.
Before this change, `ai.api_key` could appear in FE runtime logs and
audit logs when these resource statements were logged in their original
form. This PR limits the fix to masking `ai.api_key` for `CREATE/ALTER
RESOURCE` logging paths only. It does not intend to expand masking
coverage for other statement types or other logging paths in this PR.
### Release note
Mask `ai.api_key` in FE logs and audit logs for `CREATE/ALTER RESOURCE`
statements.
### Check List (For Author)
- Test:
- Unit Test
- Manual test
- Behavior changed:
- Yes. `ai.api_key` is no longer logged in plaintext for `CREATE/ALTER
RESOURCE` statements in FE logs and audit logs.
- Does this need documentation:
- No.
---
.../java/org/apache/doris/catalog/ResourceMgr.java | 3 +-
.../doris/common/util/DatasourcePrintableMap.java | 1 +
.../parser/LogicalPlanBuilderForEncryption.java | 33 ++++--
.../java/org/apache/doris/qe/StmtExecutor.java | 72 +++++++++--
.../parser/RepositoryAuditEncryptionTest.java | 35 ++++++
.../java/org/apache/doris/qe/StmtExecutorTest.java | 131 +++++++++++++++++++++
6 files changed, 258 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 92073f2cb98..98a8abb9701 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
@@ -154,7 +154,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/common/util/DatasourcePrintableMap.java
b/fe/fe-core/src/main/java/org/apache/doris/common/util/DatasourcePrintableMap.java
index 3a5e2ed3c2a..9544211dfe0 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/common/util/DatasourcePrintableMap.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/common/util/DatasourcePrintableMap.java
@@ -43,6 +43,7 @@ public class DatasourcePrintableMap<K, V> extends
BasicPrintableMap<K, V> {
SENSITIVE_KEY.add("bos_secret_accesskey");
SENSITIVE_KEY.add("jdbc.password");
SENSITIVE_KEY.add("elasticsearch.password");
+ SENSITIVE_KEY.add("ai.api_key");
SENSITIVE_KEY.addAll(Arrays.asList(
MCProperties.SECRET_KEY));
// DLF 1.0 secret keys. Formerly reflected off
AliyunDLFBaseProperties, removed with the DLF 1.0 thrift
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 03752cad308..006750dbe5d 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.DatasourcePrintableMap;
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;
@@ -203,6 +202,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) {
@@ -217,9 +227,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),
@@ -233,9 +242,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),
@@ -246,6 +254,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)) {
DatasourcePrintableMap<String, String> printableMap = new
DatasourcePrintableMap<>(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 b117b334f61..17235a7bf15 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
@@ -100,6 +100,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.SupportProfile;
@@ -182,6 +183,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();
@@ -588,7 +590,7 @@ public class StmtExecutor {
TUniqueId queryId = UniqueIdUtils.fastUniqueId();
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);
}
@@ -762,7 +764,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();
@@ -843,15 +845,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,
@@ -859,13 +862,15 @@ 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
- LOG.info("Command({}) process failed.", originStmt.originStmt,
e);
+ LOG.info("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 {
@@ -905,7 +910,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());
@@ -2403,6 +2408,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
index 0b363b123b1..70e5f59c3fe 100644
---
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
@@ -60,4 +60,39 @@ public class RepositoryAuditEncryptionTest {
Assertions.assertFalse(masked.contains("SUPERSECRET"), "secret_key
must be masked: " + masked);
Assertions.assertTrue(masked.contains("*XXX"), "expected mask token: "
+ masked);
}
+
+ @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 607cea3b40a..eb70a206b3a 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,12 +18,15 @@
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.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.planner.PlanFragment;
import org.apache.doris.planner.Planner;
import org.apache.doris.planner.ResultFileSink;
@@ -49,6 +52,8 @@ import java.util.List;
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 {
@@ -472,4 +477,130 @@ public class StmtExecutorTest extends TestWithFeService {
connectContext.getSessionVariable().cloudTableVersionCacheTtlMs =
originalTableTtl;
}
}
+
+ @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]