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]