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]

Reply via email to