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]

Reply via email to