This is an automated email from the ASF dual-hosted git repository.

Gabriel39 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 61d1aefa052 [fix](auth) Check external catalog visibility before SHOW 
DATABASES (#69612)
61d1aefa052 is described below

commit 61d1aefa0529e163e683b5a666bf2c2e3299c759
Author: Gabriel <[email protected]>
AuthorDate: Mon Oct 26 17:49:58 2026 +0800

    [fix](auth) Check external catalog visibility before SHOW DATABASES (#69612)
    
    ### What problem does this PR solve?
    
    `SHOW DATABASES` could call an external catalog's metadata service
    before checking whether the caller could see that catalog. A caller with
    no privileges could trigger remote initialization and receive connection
    errors.
    
    Check catalog visibility before listing databases or planning the
    `WHERE` form. Reuse the existing `SHOW` predicate, which includes
    database, table and column grants and honors the configured
    authorization controller. Keep the existing per-database filtering and
    internal-catalog behavior.
    
    ### Release note
    
    Check external catalog visibility before accessing metadata for `SHOW
    DATABASES`.
    
    ### Validation
    
    - Compiled the changed command and test against cached master
    dependencies; all 6 `ShowDatabasesCommandTest` tests passed.
    - Confirmed the new unauthorized-access test failed on the original
    command before applying the fix.
    - FE Checkstyle passed with zero violations.
    - No live Ranger integration test or full FE test suite was run locally.
    
    ### Check List (For Author)
    
    - Test: Unit Test
    - Behavior changed: Yes. Callers without external catalog visibility
    receive access denied before remote metadata access.
    - Does this need documentation? No.
---
 .../trees/plans/commands/ShowDatabasesCommand.java | 19 +++--
 .../plans/commands/ShowDatabasesCommandTest.java   | 92 ++++++++++++++++++++++
 2 files changed, 106 insertions(+), 5 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowDatabasesCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowDatabasesCommand.java
index 9beb5b5b39a..9c66bbd4a00 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowDatabasesCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowDatabasesCommand.java
@@ -24,6 +24,8 @@ import org.apache.doris.catalog.ScalarType;
 import org.apache.doris.catalog.info.TableNameInfo;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.CaseSensibility;
+import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.ErrorReport;
 import org.apache.doris.common.PatternMatcher;
 import org.apache.doris.common.PatternMatcherWrapper;
 import org.apache.doris.datasource.CatalogIf;
@@ -121,6 +123,17 @@ public class ShowDatabasesCommand extends ShowCommand {
     @Override
     public ShowResultSet doRun(ConnectContext ctx, StmtExecutor executor) 
throws Exception {
         validate(ctx);
+        CatalogIf catalogIf = ctx.getCatalog(catalog);
+        if (catalogIf == null) {
+            throw new AnalysisException("No catalog found with name " + 
catalog);
+        }
+        // SHOW includes visibility inherited from database/table/column 
grants. Check it before either
+        // listing remote databases or planning the WHERE form, which can 
initialize external metadata too.
+        if (!catalogIf.isInternalCatalog() && 
!Env.getCurrentEnv().getAccessManager()
+                .checkCtlPriv(ctx, catalog, PrivPredicate.SHOW)) {
+            
ErrorReport.reportAnalysisException(ErrorCode.ERR_CATALOG_ACCESS_DENIED,
+                    ctx.getQualifiedUser(), catalog);
+        }
         if (whereClause != null) {
             Expression rewrited = whereClause.accept(new 
ReplaceColumnNameVisitor(), null);
             String whereCondition = " WHERE `CATALOG_NAME` = '" + catalog
@@ -128,10 +141,6 @@ public class ShowDatabasesCommand extends ShowCommand {
             return execute(ctx, executor, whereCondition);
         }
         List<List<String>> rows = Lists.newArrayList();
-        CatalogIf catalogIf = ctx.getCatalog(catalog);
-        if (catalogIf == null) {
-            throw new AnalysisException("No catalog found with name " + 
catalog);
-        }
         List<String> dbNames = catalogIf.getDbNames();
         PatternMatcher matcher = null;
         if (likePattern != null) {
@@ -146,7 +155,7 @@ public class ShowDatabasesCommand extends ShowCommand {
                 continue;
             }
 
-            if 
(!Env.getCurrentEnv().getAccessManager().checkDbPriv(ConnectContext.get(), 
catalog,
+            if (!Env.getCurrentEnv().getAccessManager().checkDbPriv(ctx, 
catalog,
                     fullName, PrivPredicate.SHOW)) {
                 continue;
             }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowDatabasesCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowDatabasesCommandTest.java
index c4d12303ee3..38cd7c577a3 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowDatabasesCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowDatabasesCommandTest.java
@@ -17,23 +17,36 @@
 
 package org.apache.doris.nereids.trees.plans.commands;
 
+import org.apache.doris.analysis.TablePattern;
 import org.apache.doris.backup.CatalogMocker;
 import org.apache.doris.catalog.Env;
+import org.apache.doris.common.AnalysisException;
 import org.apache.doris.datasource.CatalogMgr;
+import org.apache.doris.datasource.ExternalCatalog;
 import org.apache.doris.datasource.InternalCatalog;
 import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.mysql.privilege.ColPrivilegeKey;
+import org.apache.doris.mysql.privilege.PrivBitSet;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.mysql.privilege.Privilege;
+import org.apache.doris.mysql.privilege.Role;
 import org.apache.doris.nereids.analyzer.UnboundSlot;
 import org.apache.doris.nereids.trees.expressions.EqualTo;
 import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.qe.ConnectContext;
+import org.apache.doris.qe.ShowResultSet;
 
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
+import java.util.Arrays;
+import java.util.Collections;
+
 public class ShowDatabasesCommandTest {
     private static final String internalCtl = 
InternalCatalog.INTERNAL_CATALOG_NAME;
 
@@ -80,5 +93,84 @@ public class ShowDatabasesCommandTest {
         ShowDatabasesCommand command2 = new ShowDatabasesCommand(null, null, 
equalTo);
         Assertions.assertDoesNotThrow(() -> command2.validate(ctx));
     }
+
+    @Test
+    void testDeniedExternalCatalogNeverLoadsMetadata() throws Exception {
+        ExternalCatalog external = Mockito.mock(ExternalCatalog.class);
+        Mockito.when(ctx.getCatalog("lake")).thenReturn(external);
+        Mockito.when(ctx.getDefaultCatalog()).thenReturn("lake");
+        Mockito.when(external.getDbNames()).thenThrow(new 
IllegalStateException("remote connection details"));
+        EqualTo where = new EqualTo(new UnboundSlot("Database"), new 
StringLiteral("visible"));
+        for (ShowDatabasesCommand command : Arrays.asList(
+                new ShowDatabasesCommand("lake", null, null),
+                new ShowDatabasesCommand(null, null, null),
+                new ShowDatabasesCommand("lake", "vis%", null),
+                new ShowDatabasesCommand("lake", null, where))) {
+            AnalysisException error = 
Assertions.assertThrows(AnalysisException.class,
+                    () -> command.doRun(ctx, null));
+            Assertions.assertFalse(error.getMessage().contains("remote 
connection details"));
+        }
+        Mockito.verify(external, Mockito.never()).getDbNames();
+        Mockito.verify(accessManager, Mockito.times(4)).checkCtlPriv(ctx, 
"lake", PrivPredicate.SHOW);
+    }
+
+    @Test
+    void testVisibleExternalCatalogStillFiltersDatabases() throws Exception {
+        ExternalCatalog external = Mockito.mock(ExternalCatalog.class);
+        Mockito.when(ctx.getCatalog("lake")).thenReturn(external);
+        Mockito.when(accessManager.checkCtlPriv(ctx, "lake", 
PrivPredicate.SHOW)).thenReturn(true);
+        
Mockito.when(external.getDbNames()).thenReturn(Arrays.asList("visible", 
"hidden", "other"));
+        
Mockito.when(accessManager.checkDbPriv(Mockito.nullable(ConnectContext.class),
+                Mockito.eq("lake"), Mockito.eq("visible"), 
Mockito.eq(PrivPredicate.SHOW))).thenReturn(true);
+        ShowResultSet result = new ShowDatabasesCommand("lake", null, 
null).doRun(ctx, null);
+        
Assertions.assertEquals(Collections.singletonList(Collections.singletonList("visible")),
+                result.getResultRows());
+        InOrder order = Mockito.inOrder(accessManager, external);
+        order.verify(accessManager).checkCtlPriv(ctx, "lake", 
PrivPredicate.SHOW);
+        order.verify(external).getDbNames();
+        Assertions.assertEquals(result.getResultRows(),
+                new ShowDatabasesCommand("lake", "vis%", null).doRun(ctx, 
null).getResultRows());
+    }
+
+    @Test
+    void testInternalCatalogDoesNotRequireCatalogPrivilege() throws Exception {
+        Mockito.when(catalog.isInternalCatalog()).thenReturn(true);
+        Mockito.when(ctx.getCatalog(internalCtl)).thenReturn(catalog);
+        Mockito.when(catalog.getDbNames()).thenReturn(Collections.emptyList());
+        Assertions.assertTrue(new ShowDatabasesCommand(internalCtl, null, null)
+                .doRun(ctx, null).getResultRows().isEmpty());
+        Mockito.verify(accessManager, 
Mockito.never()).checkCtlPriv(Mockito.any(ConnectContext.class),
+                Mockito.anyString(), Mockito.any(PrivPredicate.class));
+        Mockito.verify(catalog).getDbNames();
+    }
+
+    @Test
+    void testMissingCatalogKeepsExplicitError() {
+        AnalysisException error = 
Assertions.assertThrows(AnalysisException.class,
+                () -> new ShowDatabasesCommand("missing", null, 
null).doRun(ctx, null));
+        Assertions.assertTrue(error.getMessage().contains("No catalog found"));
+    }
+
+    @Test
+    void testDatabaseTableAndColumnGrantsKeepCatalogVisible() throws Exception 
{
+        ExternalCatalog external = Mockito.mock(ExternalCatalog.class);
+        Mockito.when(ctx.getCatalog("lake")).thenReturn(external);
+        
Mockito.when(external.getDbNames()).thenReturn(Arrays.asList("visible", 
"hidden"));
+        for (String level : Arrays.asList("database", "table", "column")) {
+            TablePattern pattern = new TablePattern("lake", "visible", 
level.equals("database") ? "*" : "t");
+            pattern.analyze();
+            Role role = level.equals("column")
+                    ? new Role("reader", pattern, PrivBitSet.of(), 
Collections.singletonMap(
+                            new ColPrivilegeKey(Privilege.SELECT_PRIV, "lake", 
"visible", "t"),
+                            Collections.singleton("c")))
+                    : new Role("reader", pattern, 
PrivBitSet.of(Privilege.SELECT_PRIV));
+            Mockito.when(accessManager.checkCtlPriv(ctx, "lake", 
PrivPredicate.SHOW))
+                    .thenAnswer(invocation -> role.checkCtlPriv("lake", 
PrivPredicate.SHOW, PrivBitSet.of()));
+            Mockito.when(accessManager.checkDbPriv(ctx, "lake", "visible", 
PrivPredicate.SHOW))
+                    .thenAnswer(invocation -> role.checkDbPriv("lake", 
"visible", PrivPredicate.SHOW, PrivBitSet.of()));
+            
Assertions.assertEquals(Collections.singletonList(Collections.singletonList("visible")),
+                    new ShowDatabasesCommand("lake", null, null).doRun(ctx, 
null).getResultRows(), level);
+        }
+    }
 }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to