From b05b61a43c64231a2333738db037cc08425c5248 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Sat, 8 Aug 2026 22:54:09 -0700 Subject: [PATCH 1/3] feat: User-aware SchemaProviders for SQL. This patch adds a SchemaProvider interface, which can provide schemas based on the identity of the current user. SqlBindings#addSchemaProvider can be used by extension to add such providers. In core, this patch moves the "druid", "view", and "sys" schemas to use schema providers that filter out unauthorized tables and views. This improves the behavior for unauthorized tables. Previously unauthorized tables were explicitly filtered out of InformationSchema, so users could not see them in metadata queries. However, they were visible to the validator, so a query that explicitly named such a table would return a "Forbidden" error. Now the error is "table not found". To preserve the functioning of view expansion, views are now expanded using an escalated schema. Comments about the view security model are added to ViewManager's javadoc. To ensure that table validation happens as expected at ingestion time, INSERT and REPLACE now require READ access (in addition to WRITE) on the target table. A new configuration option "druid.sql.planner.authorizeTableVisibility" (default true) is added. If set explicitly to false, the old behavior is restored. --- .../benchmark/query/InPlanningBenchmark.java | 12 +- .../benchmark/query/SqlBaseBenchmark.java | 8 +- .../benchmark/query/SqlVsNativeBenchmark.java | 12 +- .../schema/SysSegmentsTableBenchmark.java | 17 +- docs/configuration/index.md | 1 + docs/multi-stage-query/security.md | 6 +- .../auth/AbstractAuthConfigurationTest.java | 45 ++- .../druid/msq/sql/MSQTaskSqlEngine.java | 6 +- .../controller/http/DartSqlResourceTest.java | 6 +- .../msq/exec/ResultsContextSerdeTest.java | 14 +- .../apache/druid/msq/test/MSQTestBase.java | 6 +- .../server/security/AuthorizationUtils.java | 21 ++ .../apache/druid/sql/AbstractStatement.java | 1 - .../org/apache/druid/sql/DirectStatement.java | 1 + .../org/apache/druid/sql/HttpStatement.java | 1 + .../apache/druid/sql/PreparedStatement.java | 1 + .../sql/calcite/planner/CalcitePlanner.java | 6 +- .../sql/calcite/planner/IngestHandler.java | 16 +- .../sql/calcite/planner/PlannerConfig.java | 31 +- .../sql/calcite/planner/PlannerContext.java | 108 ++++--- .../sql/calcite/planner/PlannerFactory.java | 20 +- .../sql/calcite/planner/PlannerToolbox.java | 18 +- .../sql/calcite/planner/QueryHandler.java | 1 + .../planner/SqlResourceCollectorShuttle.java | 16 +- .../schema/DruidCalciteSchemaModule.java | 31 +- .../druid/sql/calcite/schema/DruidSchema.java | 45 ++- .../calcite/schema/DruidSchemaCatalog.java | 30 +- .../schema/DruidSchemaCatalogProvider.java | 40 +++ .../DruidSchemaCatalogProviderImpl.java | 115 ++++++++ .../calcite/schema/DruidSchemaProvider.java | 89 ++++++ .../sql/calcite/schema/InformationSchema.java | 166 ++++------- .../sql/calcite/schema/LookupSchema.java | 2 + .../sql/calcite/schema/NamedDruidSchema.java | 2 - .../sql/calcite/schema/NamedLookupSchema.java | 2 + .../sql/calcite/schema/NamedSystemSchema.java | 2 - .../sql/calcite/schema/NamedViewSchema.java | 2 - .../calcite/schema/RootSchemaProvider.java | 71 ----- .../sql/calcite/schema/SchemaProvider.java | 46 +++ .../druid/sql/calcite/schema/SchemaUtils.java | 84 ++++++ .../sql/calcite/schema/SystemSchema.java | 270 ++++++++++-------- .../calcite/schema/SystemSchemaProvider.java | 130 +++++++++ .../schema/SystemServerPropertiesTable.java | 11 +- .../druid/sql/calcite/schema/ViewSchema.java | 45 ++- .../calcite/schema/ViewSchemaProvider.java | 63 ++++ .../sql/calcite/view/DruidViewMacro.java | 12 +- .../druid/sql/calcite/view/ViewManager.java | 19 +- .../apache/druid/sql/guice/SqlBindings.java | 16 +- .../apache/druid/sql/SqlStatementTest.java | 9 +- .../sql/avatica/DruidAvaticaHandlerTest.java | 87 +++--- .../druid/sql/avatica/DruidStatementTest.java | 12 +- .../sql/calcite/BaseCalciteQueryTest.java | 10 - .../CalciteCatalogIngestionDmlTest.java | 40 ++- .../druid/sql/calcite/CalciteExportTest.java | 2 +- .../sql/calcite/CalciteIngestionDmlTest.java | 6 + .../sql/calcite/CalciteInsertDmlTest.java | 94 ++++-- .../druid/sql/calcite/CalciteQueryTest.java | 135 +++++++-- .../sql/calcite/CalciteReplaceDmlTest.java | 37 +-- .../sql/calcite/CalciteSelectQueryTest.java | 162 +++++++---- .../sql/calcite/CalciteStrictInsertTest.java | 2 +- .../DruidPlannerResourceAnalyzeTest.java | 83 ++++++ .../sql/calcite/IngestTableFunctionTest.java | 34 +-- ...orizedExpressionResultConsistencyTest.java | 8 +- .../expression/ExpressionTestHelper.java | 15 +- .../external/ExternalTableScanRuleTest.java | 14 +- .../planner/CalcitePlannerModuleTest.java | 8 +- .../calcite/planner/DruidRexExecutorTest.java | 14 +- .../ConstantDruidSchemaCatalogProvider.java | 49 ++++ .../schema/DruidCalciteSchemaModuleTest.java | 131 ++++----- ...=> DruidSchemaProviderNoDataInitTest.java} | 28 +- .../calcite/schema/InformationSchemaTest.java | 12 +- .../schema/RootSchemaProviderTest.java | 94 ------ .../sql/calcite/schema/SystemSchemaTest.java | 252 +++++++++------- .../druid/sql/calcite/util/CalciteTests.java | 33 +-- .../sql/calcite/util/QueryFrameworkUtils.java | 120 ++++---- .../sql/calcite/util/SqlTestFramework.java | 54 ++-- .../sql/calcite/util/TestAuthorizer.java | 13 + .../sql/calcite/util/TestDataBuilder.java | 9 + .../util/TestDruidViewMacroFactory.java | 7 +- .../druid/sql/http/SqlResourceTest.java | 23 +- 79 files changed, 2139 insertions(+), 1125 deletions(-) create mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalogProvider.java create mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalogProviderImpl.java create mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaProvider.java delete mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/schema/RootSchemaProvider.java create mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/schema/SchemaProvider.java create mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/schema/SchemaUtils.java create mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchemaProvider.java create mode 100644 sql/src/main/java/org/apache/druid/sql/calcite/schema/ViewSchemaProvider.java create mode 100644 sql/src/test/java/org/apache/druid/sql/calcite/schema/ConstantDruidSchemaCatalogProvider.java rename sql/src/test/java/org/apache/druid/sql/calcite/schema/{DruidSchemaNoDataInitTest.java => DruidSchemaProviderNoDataInitTest.java} (75%) delete mode 100644 sql/src/test/java/org/apache/druid/sql/calcite/schema/RootSchemaProviderTest.java diff --git a/benchmarks/src/test/java/org/apache/druid/benchmark/query/InPlanningBenchmark.java b/benchmarks/src/test/java/org/apache/druid/benchmark/query/InPlanningBenchmark.java index 565796375845..44f9ba6c7572 100644 --- a/benchmarks/src/test/java/org/apache/druid/benchmark/query/InPlanningBenchmark.java +++ b/benchmarks/src/test/java/org/apache/druid/benchmark/query/InPlanningBenchmark.java @@ -53,7 +53,7 @@ import org.apache.druid.sql.calcite.planner.PlannerFactory; import org.apache.druid.sql.calcite.planner.PlannerResult; import org.apache.druid.sql.calcite.run.SqlEngine; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.hook.DruidHookDispatcher; import org.apache.druid.timeline.DataSegment; @@ -186,13 +186,17 @@ public void setup() throws JsonProcessingException ); closer.register(walker); final ObjectMapper jsonMapper = CalciteTests.getJsonMapper(); - final DruidSchemaCatalog rootSchema = - CalciteTests.createMockRootSchema(conglomerate, walker, plannerConfig, AuthTestUtils.TEST_AUTHORIZER_MAPPER); + final DruidSchemaCatalogProvider rootSchemaProvider = CalciteTests.createMockRootSchemaProvider( + conglomerate, + walker, + plannerConfig, + AuthTestUtils.TEST_AUTHORIZER_MAPPER + ); engine = CalciteTests.createMockSqlEngine(walker, conglomerate); plannerFactory = new PlannerFactory( - rootSchema, + rootSchemaProvider, CalciteTests.createOperatorTable(), CalciteTests.createExprMacroTable(), plannerConfig, diff --git a/benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlBaseBenchmark.java b/benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlBaseBenchmark.java index b787244373b4..f821a31a4de2 100644 --- a/benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlBaseBenchmark.java +++ b/benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlBaseBenchmark.java @@ -100,7 +100,7 @@ import org.apache.druid.sql.calcite.planner.PlannerFactory; import org.apache.druid.sql.calcite.planner.PlannerResult; import org.apache.druid.sql.calcite.run.SqlEngine; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.calcite.util.LookylooModule; import org.apache.druid.sql.calcite.util.QueryFrameworkUtils; @@ -480,8 +480,8 @@ public static Pair createSqlSystem( ObjectMapper injected = injector.getInstance(Key.get(ObjectMapper.class, Json.class)); injected.registerModules(new HllSketchModule().getJacksonModules()); - final DruidSchemaCatalog rootSchema = - QueryFrameworkUtils.createMockRootSchema( + final DruidSchemaCatalogProvider schemaProvider = + QueryFrameworkUtils.createMockRootSchemaProvider( injector, conglomerate, walker, @@ -492,7 +492,7 @@ public static Pair createSqlSystem( final SqlEngine engine = CalciteTests.createMockSqlEngine(walker, conglomerate); final PlannerFactory plannerFactory = new PlannerFactory( - rootSchema, + schemaProvider, createOperatorTable(injector), injector.getInstance(ExprMacroTable.class), plannerConfig, diff --git a/benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlVsNativeBenchmark.java b/benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlVsNativeBenchmark.java index 1900447fbfaf..e7ccaf5cea29 100644 --- a/benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlVsNativeBenchmark.java +++ b/benchmarks/src/test/java/org/apache/druid/benchmark/query/SqlVsNativeBenchmark.java @@ -48,7 +48,7 @@ import org.apache.druid.sql.calcite.planner.PlannerFactory; import org.apache.druid.sql.calcite.planner.PlannerResult; import org.apache.druid.sql.calcite.run.SqlEngine; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.hook.DruidHookDispatcher; import org.apache.druid.timeline.DataSegment; @@ -115,11 +115,15 @@ public void setup() final PlannerConfig plannerConfig = new PlannerConfig(); this.walker = closer.register(SpecificSegmentsQuerySegmentWalker.createWalker(conglomerate).add(dataSegment, index)); - final DruidSchemaCatalog rootSchema = - CalciteTests.createMockRootSchema(conglomerate, walker, plannerConfig, AuthTestUtils.TEST_AUTHORIZER_MAPPER); + final DruidSchemaCatalogProvider rootSchemaProvider = CalciteTests.createMockRootSchemaProvider( + conglomerate, + walker, + plannerConfig, + AuthTestUtils.TEST_AUTHORIZER_MAPPER + ); engine = CalciteTests.createMockSqlEngine(walker, conglomerate); plannerFactory = new PlannerFactory( - rootSchema, + rootSchemaProvider, CalciteTests.createOperatorTable(), CalciteTests.createExprMacroTable(), plannerConfig, diff --git a/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java b/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java index 9eadf5bffeef..d75f46c89eef 100644 --- a/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java +++ b/benchmarks/src/test/java/org/apache/druid/sql/calcite/schema/SysSegmentsTableBenchmark.java @@ -35,6 +35,7 @@ import org.apache.druid.client.InternalQueryConfig; import org.apache.druid.client.TimelineServerView; import org.apache.druid.client.coordinator.NoopCoordinatorClient; +import org.apache.druid.error.NotYetImplemented; import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.CloseableIterators; import org.apache.druid.java.util.common.Intervals; @@ -50,8 +51,6 @@ import org.apache.druid.server.security.Authorizer; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Escalator; -import org.apache.druid.sql.calcite.planner.CatalogResolver; -import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; import org.apache.druid.timeline.SegmentStatusInCluster; @@ -202,9 +201,6 @@ public ListenableFuture> fetchAllUsedS new NoopServiceEmitter() ); - final DruidSchema druidSchema = - new DruidSchema(new EmptyBrokerSegmentMetadataCache(), null, CatalogResolver.NULL_RESOLVER); - final AuthorizerMapper authorizerMapper = new AuthorizerMapper(null) { @Override @@ -214,11 +210,16 @@ public Authorizer getAuthorizer(String name) } }; - segmentsTable = new SystemSchema.SegmentsTable(druidSchema, metadataView, new DefaultObjectMapper(), authorizerMapper); + segmentsTable = new SystemSchema.SegmentsTable( + new EmptyBrokerSegmentMetadataCache(), + metadataView, + new DefaultObjectMapper(), + authorizerMapper, + new AuthenticationResult("benchmark", "benchmark", null, null) + ); filtersByQuery = buildFilters(); - final AuthenticationResult authenticationResult = new AuthenticationResult("benchmark", "benchmark", null, null); dataContext = new DataContext() { @Override @@ -242,7 +243,7 @@ public QueryProvider getQueryProvider() @Override public Object get(String name) { - return PlannerContext.DATA_CTX_AUTHENTICATION_RESULT.equals(name) ? authenticationResult : null; + throw NotYetImplemented.ex(null, "Not expected to be called"); } }; } diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 238f0b357faa..85db7a87c812 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -1899,6 +1899,7 @@ The Druid SQL server is configured through the following properties on the Broke |`druid.sql.planner.metadataSegmentCacheEnable`|Whether to keep a cache of published segments on Broker that can be used to serve queries against `sys.segments`. If true, broker polls coordinator in background to get segments from metadata store and maintains a local cache. If false, coordinator's REST API will be invoked when broker needs published segments info.|true| |`druid.sql.planner.metadataSegmentPollPeriod`|How often to poll coordinator for published segments list if `druid.sql.planner.metadataSegmentCacheEnable` is set to true. Poll period is in milliseconds. |60000| |`druid.sql.planner.authorizeSystemTablesDirectly`|If true, Druid authorizes queries against any of the system schema tables (`sys` in SQL) as `SYSTEM_TABLE` resources which require `READ` access, in addition to permissions based content filtering.|false| +|`druid.sql.planner.authorizeTableVisibility`|Whether [READ DATASOURCE](../multi-stage-query/security.md) permissions are required for table visibility in the validator. When this is set, users that query unauthorized tables see a "not found" error rather than "forbidden". Additionally, when this is set, INSERT and REPLACE require both READ and WRITE access to the target table. (If this property is not set, they require only WRITE.) Regardless of the value of this property, READ access is required for tables to show up in the INFORMATION_SCHEMA.|true| |`druid.sql.planner.useNativeQueryExplain`|If true, `EXPLAIN PLAN FOR` will return the explain plan as a JSON representation of equivalent native query(s), else it will return the original version of explain plan generated by Calcite. It can be overridden per query with `useNativeQueryExplain` context key.|true| |`druid.sql.planner.maxNumericInFilters`|Max limit for the amount of numeric values that can be compared for a string type dimension when the entire SQL WHERE clause of a query translates to an [OR](../querying/filters.md#or) of [Bound filter](../querying/filters.md#bound-filter). By default, Druid does not restrict the amount of numeric Bound Filters on String columns, although this situation may block other queries from running. Set this property to a smaller value to prevent Druid from running queries that have prohibitively long segment processing times. The optimal limit requires some trial and error; we recommend starting with 100. Users who submit a query that exceeds the limit of `maxNumericInFilters` should instead rewrite their queries to use strings in the `WHERE` clause instead of numbers. For example, `WHERE someString IN (‘123’, ‘456’)`. If this value is disabled, `maxNumericInFilters` set through query context is ignored.|`-1` (disabled)| |`druid.sql.approxCountDistinct.function`|Implementation to use for the [`APPROX_COUNT_DISTINCT` function](../querying/sql-aggregations.md). Without extensions loaded, the only valid value is `APPROX_COUNT_DISTINCT_BUILTIN` (a HyperLogLog, or HLL, based implementation). If the [DataSketches extension](../development/extensions-core/datasketches-extension.md) is loaded, this can also be `APPROX_COUNT_DISTINCT_DS_HLL` (alternative HLL implementation) or `APPROX_COUNT_DISTINCT_DS_THETA`.

Theta sketches use significantly more memory than HLL sketches, so you should prefer one of the two HLL implementations.|`APPROX_COUNT_DISTINCT_BUILTIN`| diff --git a/docs/multi-stage-query/security.md b/docs/multi-stage-query/security.md index 0a50b68d4d6f..f0f9ff74b839 100644 --- a/docs/multi-stage-query/security.md +++ b/docs/multi-stage-query/security.md @@ -30,8 +30,10 @@ data. The permission needed depends on what the user is trying to do. To submit a query: - SELECT from a Druid datasource requires the READ DATASOURCE permission on that datasource. -- [INSERT](reference.md#insert) or [REPLACE](reference.md#replace) into a Druid datasource requires the WRITE DATASOURCE - permission on that datasource. +- [INSERT](reference.md#insert) or [REPLACE](reference.md#replace) into a Druid datasource require the WRITE DATASOURCE + and READ DATASOURCE permissions on the target datasource. (Special case: if + [`druid.sql.planner.authorizeTableVisibility = false`](../configuration/index.md#sql) is set, only WRITE DATASOURCE + is required.) - [EXTERN](reference.md#extern-function) and the input-source-specific table functions require READ permission on a resource named "EXTERNAL" with type "EXTERNAL". Users without the correct permission encounter a 403 error when trying to run queries that include `EXTERN`. diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/auth/AbstractAuthConfigurationTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/auth/AbstractAuthConfigurationTest.java index 851b8caebfdf..de9d49a28cd7 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/auth/AbstractAuthConfigurationTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/auth/AbstractAuthConfigurationTest.java @@ -38,7 +38,6 @@ import org.apache.druid.query.QueryContexts; import org.apache.druid.query.http.SqlTaskStatus; import org.apache.druid.segment.TestHelper; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; import org.apache.druid.server.security.Resource; import org.apache.druid.server.security.ResourceAction; @@ -338,30 +337,32 @@ public void test_systemSchemaAccess_datasourceOnlyUser() getServerUrl(broker) + "/druid/v2/datasources/auth_test" ); - // as user that can only read auth_test - final String expectedMsg = "{\"Access-Check-Result\":\"" + Access.DEFAULT_ERROR_MESSAGE + "\"}"; - verifySystemSchemaQueryIsForbidden( + // As a user that can only read auth_test. This cluster runs with the default + // druid.sql.planner.authorizeTableVisibility = true, so the sys tables are not visible to this user at all and + // naming one is a validation error rather than an authorization error. (With authorizeTableVisibility = false + // these would instead be forbidden.) + verifySystemSchemaQueryIsNotFound( datasourceOnlyUserClient, SYS_SCHEMA_SEGMENTS_QUERY, - expectedMsg + "segments" ); - verifySystemSchemaQueryIsForbidden( + verifySystemSchemaQueryIsNotFound( datasourceOnlyUserClient, SYS_SCHEMA_SERVERS_QUERY, - expectedMsg + "servers" ); - verifySystemSchemaQueryIsForbidden( + verifySystemSchemaQueryIsNotFound( datasourceOnlyUserClient, SYS_SCHEMA_SERVER_SEGMENTS_QUERY, - expectedMsg + "server_segments" ); - verifySystemSchemaQueryIsForbidden( + verifySystemSchemaQueryIsNotFound( datasourceOnlyUserClient, SYS_SCHEMA_TASKS_QUERY, - expectedMsg + "tasks" ); } @@ -874,6 +875,28 @@ private void verifySystemSchemaQueryIsForbidden( Assertions.assertEquals(responseHolder.getContent(), expectedErrorMessage); } + /** + * Verifies that a sys table is not visible to the user at all, i.e. that naming it is a validation error rather + * than an authorization error. This is the behavior when {@code druid.sql.planner.authorizeTableVisibility} is + * true (the default) and the user lacks READ on the corresponding SYSTEM_TABLE resource. + */ + private void verifySystemSchemaQueryIsNotFound( + HttpClient client, + String query, + String expectedMissingTable + ) + { + final StatusResponseHolder responseHolder = + makeSQLQueryRequest(client, query, HttpResponseStatus.BAD_REQUEST); + Assertions.assertEquals(HttpResponseStatus.BAD_REQUEST, responseHolder.getStatus()); + + final String content = responseHolder.getContent(); + Assertions.assertTrue( + content.contains("Object '" + expectedMissingTable + "' not found within 'sys'"), + StringUtils.format("Expected [%s] to be not found within 'sys', but got[%s]", expectedMissingTable, content) + ); + } + protected String getBrokerAvacticaUrl() { return "jdbc:avatica:remote:url=" + getServerUrl(broker) + DruidAvaticaJsonHandler.AVATICA_PATH; diff --git a/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskSqlEngine.java b/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskSqlEngine.java index 2c05fdd606ca..8378099a9942 100644 --- a/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskSqlEngine.java +++ b/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskSqlEngine.java @@ -206,11 +206,7 @@ public QueryMaker buildQueryMakerForInsert( validateInsert( relRoot, destination instanceof TableDestination - ? plannerContext.getPlannerToolbox() - .rootSchema() - .getNamedSchema(plannerContext.getPlannerToolbox().druidSchemaName()) - .getSchema() - .getTable(((TableDestination) destination).getTableName()) + ? plannerContext.getDruidTable(((TableDestination) destination).getTableName()) : null, plannerContext ); diff --git a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java index e579ee427d7d..6fce3cf71cb6 100644 --- a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java +++ b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java @@ -79,7 +79,7 @@ import org.apache.druid.sql.calcite.planner.CatalogResolver; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.planner.PlannerFactory; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.NoopDruidSchemaManager; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.calcite.util.QueryFrameworkUtils; @@ -186,7 +186,7 @@ void setUp() { mockCloser = MockitoAnnotations.openMocks(this); - final DruidSchemaCatalog rootSchema = QueryFrameworkUtils.createMockRootSchema( + final DruidSchemaCatalogProvider schemaProvider = QueryFrameworkUtils.createMockRootSchemaProvider( CalciteTests.INJECTOR, queryFramework().conglomerate(), queryFramework().walker(), @@ -198,7 +198,7 @@ void setUp() ); final PlannerFactory plannerFactory = new PlannerFactory( - rootSchema, + schemaProvider, queryFramework().operatorTable(), queryFramework().macroTable(), PLANNER_CONFIG_DEFAULT, diff --git a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/ResultsContextSerdeTest.java b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/ResultsContextSerdeTest.java index eb0139f0e423..d7a2a2e2b106 100644 --- a/multi-stage-query/src/test/java/org/apache/druid/msq/exec/ResultsContextSerdeTest.java +++ b/multi-stage-query/src/test/java/org/apache/druid/msq/exec/ResultsContextSerdeTest.java @@ -39,6 +39,7 @@ import org.apache.druid.sql.calcite.planner.PlannerToolbox; import org.apache.druid.sql.calcite.run.NativeSqlEngine; import org.apache.druid.sql.calcite.run.SqlResults; +import org.apache.druid.sql.calcite.schema.ConstantDruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchema; import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; import org.apache.druid.sql.calcite.schema.NamedDruidSchema; @@ -66,11 +67,13 @@ public void setUp() CalciteTests.createExprMacroTable(), CalciteTests.getJsonMapper(), new PlannerConfig(), - new DruidSchemaCatalog( - EasyMock.createMock(SchemaPlus.class), - ImmutableMap.of( - "druid", new NamedDruidSchema(EasyMock.createMock(DruidSchema.class), "druid"), - NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) + new ConstantDruidSchemaCatalogProvider( + new DruidSchemaCatalog( + EasyMock.createMock(SchemaPlus.class), + ImmutableMap.of( + "druid", new NamedDruidSchema(EasyMock.createMock(DruidSchema.class), "druid"), + NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) + ) ) ), CalciteTests.createJoinableFactoryWrapper(), @@ -93,6 +96,7 @@ NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) sql, DruidSqlParser.parse(sql, false).getMainStatement(), engine, + null, // No authentication result needed for this test Collections.emptySet(), Collections.emptyMap(), null diff --git a/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java b/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java index b70d17a780e4..32d0ad4ffec7 100644 --- a/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java +++ b/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java @@ -199,7 +199,7 @@ import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.planner.PlannerFactory; import org.apache.druid.sql.calcite.run.SqlEngine; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.NoopDruidSchemaManager; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.calcite.util.DruidModuleCollection; @@ -612,7 +612,7 @@ public String getFormatString() ); CatalogResolver catalogResolver = createMockCatalogResolver(); final InProcessViewManager viewManager = new InProcessViewManager(SqlTestFramework.DRUID_VIEW_MACRO_FACTORY); - DruidSchemaCatalog rootSchema = QueryFrameworkUtils.createMockRootSchema( + DruidSchemaCatalogProvider schemaProvider = QueryFrameworkUtils.createMockRootSchemaProvider( CalciteTests.INJECTOR, qf.conglomerate(), qf.walker(), @@ -654,7 +654,7 @@ public Authorizer getAuthorizer(String name) } }; PlannerFactory plannerFactory = new PlannerFactory( - rootSchema, + schemaProvider, qf.operatorTable(), qf.macroTable(), PLANNER_CONFIG_DEFAULT, diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java index 687685f45f12..f04024db1f97 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java @@ -80,6 +80,27 @@ public static AuthorizationResult authorizeResourceAction( ); } + /** + * Performs authorization check on a single resource-action based on an {@link AuthenticationResult}. + * + * @param authenticationResult Authentication result representing identity of requester + * @param resourceAction A resource identifier and the action to be taken the resource. + * @param authorizerMapper The singleton AuthorizerMapper instance + * @return AuthorizationResult containing allow/deny access to the resource action, along with policy restrictions. + */ + public static AuthorizationResult authorizeResourceAction( + final AuthenticationResult authenticationResult, + final ResourceAction resourceAction, + final AuthorizerMapper authorizerMapper + ) + { + return authorizeAllResourceActions( + authenticationResult, + Collections.singletonList(resourceAction), + authorizerMapper + ); + } + /** * Verifies that the user has unrestricted access to perform the required * action on the given datasource. diff --git a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java index 35b9883b2594..27c41c7eb23a 100644 --- a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java @@ -129,7 +129,6 @@ public void setHook(PlannerHook hook) protected void validate(final DruidPlanner planner) { plannerContext = planner.getPlannerContext(); - plannerContext.setAuthenticationResult(queryPlus.authResult()); plannerContext.setParameters(queryPlus.parameters()); planner.validate(); } diff --git a/sql/src/main/java/org/apache/druid/sql/DirectStatement.java b/sql/src/main/java/org/apache/druid/sql/DirectStatement.java index f206194c732f..ee50d0b8e18f 100644 --- a/sql/src/main/java/org/apache/druid/sql/DirectStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/DirectStatement.java @@ -235,6 +235,7 @@ protected DruidPlanner createPlanner() sqlToolbox.engine, queryPlus.sql(), queryPlus.sqlNode(), + queryPlus.authResult(), queryPlus.authContextKeys(), queryContext, hook diff --git a/sql/src/main/java/org/apache/druid/sql/HttpStatement.java b/sql/src/main/java/org/apache/druid/sql/HttpStatement.java index ea58789302ad..3c126a108877 100644 --- a/sql/src/main/java/org/apache/druid/sql/HttpStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/HttpStatement.java @@ -63,6 +63,7 @@ protected DruidPlanner createPlanner() sqlToolbox.engine, queryPlus.sql(), queryPlus.sqlNode(), + queryPlus.authResult(), queryPlus.authContextKeys(), queryContext, hook diff --git a/sql/src/main/java/org/apache/druid/sql/PreparedStatement.java b/sql/src/main/java/org/apache/druid/sql/PreparedStatement.java index 7fa6def5879b..cd2477b322f8 100644 --- a/sql/src/main/java/org/apache/druid/sql/PreparedStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/PreparedStatement.java @@ -108,6 +108,7 @@ protected DruidPlanner getPlanner() sqlToolbox.engine, queryPlus.sql(), queryPlus.freshCopy().sqlNode(), + queryPlus.authResult(), queryPlus.authContextKeys(), queryContext, hook diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalcitePlanner.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalcitePlanner.java index de02ce864dcb..fa8f10ff7768 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalcitePlanner.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalcitePlanner.java @@ -381,8 +381,12 @@ public RelRoot expandView( throw new RuntimeException("parse failed", e); } + // Use an escalated schema for view expansion. See ViewManager javadoc for details on the security model. + final SchemaPlus viewRoot = context.unwrapOrThrow(PlannerContext.class) + .getEscalatedRootSchema() + .getRootSchema(); final CalciteCatalogReader catalogReader = - createCatalogReader().withSchemaPath(schemaPath); + new CalciteCatalogReader(CalciteSchema.from(viewRoot), schemaPath, getTypeFactory(), connectionConfig); final SqlValidator validator = createSqlValidator(catalogReader); final RexBuilder rexBuilder = createRexBuilder(); diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/IngestHandler.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/IngestHandler.java index 49a943aded76..1098e1adfb7c 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/IngestHandler.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/IngestHandler.java @@ -54,6 +54,7 @@ import org.apache.druid.sql.destination.TableDestination; import org.apache.druid.storage.ExportStorageProvider; +import java.util.ArrayList; import java.util.List; public abstract class IngestHandler extends QueryHandler @@ -215,7 +216,7 @@ private IngestDestination validateAndGetDataSourceForIngest() String tableName = Iterables.getOnlyElement(tableIdentifier.names); IdUtils.validateId("table", tableName); dataSource = new TableDestination(tableName); - resourceActions.add(new ResourceAction(new Resource(tableName, ResourceType.DATASOURCE), Action.WRITE)); + resourceActions.addAll(getIngestResourceActions(tableName)); } else { // Qualified name. final String defaultSchemaName = @@ -225,7 +226,7 @@ private IngestDestination validateAndGetDataSourceForIngest() String tableName = tableIdentifier.names.get(1); IdUtils.validateId("table", tableName); dataSource = new TableDestination(tableName); - resourceActions.add(new ResourceAction(new Resource(tableName, ResourceType.DATASOURCE), Action.WRITE)); + resourceActions.addAll(getIngestResourceActions(tableName)); } else { throw InvalidSqlInput.exception( "Table [%s] does not support operation [%s] because it is not a Druid datasource", @@ -254,6 +255,17 @@ protected QueryMaker buildQueryMaker(final RelRoot rootQueryRel) throws Validati ); } + private List getIngestResourceActions(final String dataSource) + { + final List resourceActions = new ArrayList<>(); + final Resource resource = new Resource(dataSource, ResourceType.DATASOURCE); + resourceActions.add(new ResourceAction(resource, Action.WRITE)); + if (handlerContext.plannerContext().getPlannerConfig().isAuthorizeTableVisibility()) { + resourceActions.add(new ResourceAction(resource, Action.READ)); + } + return resourceActions; + } + /** * Handler for the INSERT statement. */ diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java index d8c77c695894..d07450347f74 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java @@ -83,6 +83,9 @@ public class PlannerConfig @JsonProperty private boolean enableSysQueriesTable = false; + @JsonProperty + private boolean authorizeTableVisibility = true; + public int getMaxNumericInFilters() { return maxNumericInFilters; @@ -160,6 +163,20 @@ public boolean isEnableSysQueriesTable() return enableSysQueriesTable; } + /** + * Returns whether READ access is required for a table to be visible to the validator. + * + *

When this is set, in order to ensure that validation works properly for ingestion, INSERT and REPLACE + * require both READ and WRITE access. (If this property is not set, they require only WRITE.) + * + *

Regardless of the value of this property, READ access is required for tables to show up in + * the INFORMATION_SCHEMA. + */ + public boolean isAuthorizeTableVisibility() + { + return authorizeTableVisibility; + } + public PlannerConfig withOverrides(final Map queryContext) { if (queryContext.isEmpty()) { @@ -189,6 +206,7 @@ public boolean equals(Object o) && forceExpressionVirtualColumns == that.forceExpressionVirtualColumns && maxNumericInFilters == that.maxNumericInFilters && enableSysQueriesTable == that.enableSysQueriesTable + && authorizeTableVisibility == that.authorizeTableVisibility && Objects.equals(sqlTimeZone, that.sqlTimeZone) && Objects.equals(nativeQuerySqlPlanningMode, that.nativeQuerySqlPlanningMode); } @@ -210,7 +228,8 @@ public int hashCode() forceExpressionVirtualColumns, maxNumericInFilters, nativeQuerySqlPlanningMode, - enableSysQueriesTable + enableSysQueriesTable, + authorizeTableVisibility ); } @@ -227,6 +246,7 @@ public String toString() ", useNativeQueryExplain=" + useNativeQueryExplain + ", nativeQuerySqlPlanningMode=" + nativeQuerySqlPlanningMode + ", enableSysQueriesTable=" + enableSysQueriesTable + + ", authorizeTableVisibility=" + authorizeTableVisibility + '}'; } @@ -262,6 +282,7 @@ public static class Builder private int maxNumericInFilters; private String nativeQuerySqlPlanningMode; private boolean enableSysQueriesTable; + private boolean authorizeTableVisibility; public Builder(PlannerConfig base) { @@ -282,6 +303,7 @@ public Builder(PlannerConfig base) maxNumericInFilters = base.getMaxNumericInFilters(); nativeQuerySqlPlanningMode = base.getNativeQuerySqlPlanningMode(); enableSysQueriesTable = base.isEnableSysQueriesTable(); + authorizeTableVisibility = base.isAuthorizeTableVisibility(); } public Builder requireTimeCondition(boolean option) @@ -362,6 +384,12 @@ public Builder enableSysQueriesTable(boolean option) return this; } + public Builder authorizeTableVisibility(boolean option) + { + this.authorizeTableVisibility = option; + return this; + } + public Builder withOverrides(final Map queryContext) { useApproximateCountDistinct = QueryContexts.parseBoolean( @@ -459,6 +487,7 @@ public PlannerConfig build() config.forceExpressionVirtualColumns = forceExpressionVirtualColumns; config.nativeQuerySqlPlanningMode = nativeQuerySqlPlanningMode; config.enableSysQueriesTable = enableSysQueriesTable; + config.authorizeTableVisibility = authorizeTableVisibility; return config; } } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java index 859d1d5715c4..fc03979d7617 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java @@ -28,6 +28,7 @@ import org.apache.calcite.avatica.remote.TypedValue; import org.apache.calcite.linq4j.QueryProvider; import org.apache.calcite.schema.SchemaPlus; +import org.apache.calcite.schema.Table; import org.apache.calcite.sql.SqlNode; import org.apache.druid.error.InvalidSqlInput; import org.apache.druid.java.util.common.DateTimes; @@ -52,6 +53,7 @@ import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.expression.SqlOperatorConversion; import org.apache.druid.sql.calcite.expression.builtin.QueryLookupOperatorConversion; import org.apache.druid.sql.calcite.rel.VirtualColumnRegistry; @@ -60,6 +62,9 @@ import org.apache.druid.sql.calcite.run.EngineFeature; import org.apache.druid.sql.calcite.run.QueryMaker; import org.apache.druid.sql.calcite.run.SqlEngine; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; +import org.apache.druid.sql.calcite.view.ViewManager; import org.apache.druid.sql.hook.DruidHook.HookKey; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -138,6 +143,8 @@ public class PlannerContext private final String sql; private final SqlNode sqlNode; private final SqlEngine engine; + private final DruidSchemaCatalog rootSchema; + private final AuthenticationResult authenticationResult; private final Set authContextKeys; private final Map queryContext; private final CopyOnWriteArrayList nativeQueryIds = new CopyOnWriteArrayList<>(); @@ -145,6 +152,11 @@ public class PlannerContext private final Set lookupsToLoad = new HashSet<>(); private PlannerConfig plannerConfig; + /** + * Root schema created with {@link DruidSchemaCatalogProvider#createEscalatedRootSchema()}, for use in + * view expansion. See {@link ViewManager} for details on the security model. + */ + private DruidSchemaCatalog escalatedRootSchema; private String sqlQueryId; private boolean stringifyArrays; private boolean useBoundsAndSelectors; @@ -156,8 +168,6 @@ public class PlannerContext // bindings for dynamic parameters to bind during planning private List parameters = Collections.emptyList(); - // result of authentication, providing identity to authorize set of resources produced by validation - private AuthenticationResult authenticationResult; // set of datasources and views which must be authorized, initialized to null so we can detect if it has been set. private Set resourceActions; // result of authorizing set of resources against authentication identity @@ -176,6 +186,8 @@ private PlannerContext( final String sql, final SqlNode sqlNode, final SqlEngine engine, + final DruidSchemaCatalog rootSchema, + final AuthenticationResult authenticationResult, final Set authContextKeys, final Map queryContext, final PlannerHook hook @@ -186,6 +198,8 @@ private PlannerContext( this.sql = sql; this.sqlNode = sqlNode; this.engine = engine; + this.rootSchema = rootSchema; + this.authenticationResult = authenticationResult; this.authContextKeys = authContextKeys; this.queryContext = new LinkedHashMap<>(queryContext); this.hook = hook == null ? NoOpPlannerHook.INSTANCE : hook; @@ -197,6 +211,7 @@ public static PlannerContext create( final String sql, final SqlNode sqlNode, final SqlEngine engine, + final AuthenticationResult authenticationResult, final Set authContextKeys, final Map queryContext, final PlannerHook hook @@ -207,6 +222,8 @@ public static PlannerContext create( sql, sqlNode, engine, + plannerToolbox.rootSchemaProvider.createRootSchema(authenticationResult), + authenticationResult, authContextKeys, queryContext, hook @@ -229,26 +246,6 @@ public static JoinAlgorithm getJoinAlgorithm(Map queryContext) return getJoinAlgorithmFromContextValue(queryContext.get(CTX_SQL_JOIN_ALGORITHM)); } - private static JoinAlgorithm getJoinAlgorithmFromContextValue(final Object object) - { - final String s = QueryContexts.getAsString( - CTX_SQL_JOIN_ALGORITHM, - object, - DEFAULT_SQL_JOIN_ALGORITHM.toString() - ); - - try { - return JoinAlgorithm.fromString(s); - } - catch (IllegalArgumentException e) { - throw QueryContexts.badValueException( - CTX_SQL_JOIN_ALGORITHM, - StringUtils.format("one of %s", Arrays.toString(JoinAlgorithm.values())), - object - ); - } - } - public PlannerToolbox getPlannerToolbox() { return plannerToolbox; @@ -264,7 +261,6 @@ public ExpressionParser getExpressionParser() return expressionParser; } - /** * Equivalent to {@link ExpressionParser#parse(String)} on {@link #getExpressionParser()}. */ @@ -299,10 +295,44 @@ public JoinableFactoryWrapper getJoinableFactoryWrapper() return plannerToolbox.joinableFactoryWrapper(); } + /** + * Returns the {@link Table} object corresponding to a Druid table/datasource, or null if none exists. + */ + @Nullable + public Table getDruidTable(String tableName) + { + return rootSchema.getSubSchema(plannerToolbox.druidSchemaName()).tables().get(tableName); + } + + /** + * Returns the root schema created with {@link DruidSchemaCatalogProvider#createRootSchema(AuthenticationResult)}, + * scoped for {@link #getAuthenticationResult()}. + */ + public DruidSchemaCatalog getRootSchema() + { + return rootSchema; + } + + /** + * Returns the root schema created with {@link DruidSchemaCatalogProvider#createEscalatedRootSchema()}, + * for use in view expansion. See {@link ViewManager} for details on the security model. + */ + public DruidSchemaCatalog getEscalatedRootSchema() + { + if (escalatedRootSchema == null) { + escalatedRootSchema = plannerToolbox.rootSchemaProvider.createEscalatedRootSchema(); + } + return escalatedRootSchema; + } + + /** + * Returns the {@link ResourceType} string for a particular resource (e.g. table, view) located in a + * particular schema. If null, there is no authorization associated with the named resource. + */ @Nullable public String getSchemaResourceType(String schema, String resourceName) { - return plannerToolbox.rootSchema().getResourceType(schema, resourceName); + return rootSchema.getResourceType(schema, resourceName); } /** @@ -542,16 +572,6 @@ public void setParameters(List parameters) this.parameters = Preconditions.checkNotNull(parameters, "parameters"); } - public void setAuthenticationResult(AuthenticationResult authenticationResult) - { - if (this.authenticationResult != null) { - // It's a bug if this happens, because setAuthenticationResult should be called exactly once. - throw new ISE("Authentication result has already been set"); - } - - this.authenticationResult = Preconditions.checkNotNull(authenticationResult, "authenticationResult"); - } - public void setAuthorizationResult(AuthorizationResult access) { if (this.authorizationResult != null) { @@ -746,4 +766,24 @@ private void initializeContextFieldsAndPlannerConfig() plannerConfig = getPlannerToolbox().plannerConfig.withOverrides(queryContext); } } + + private static JoinAlgorithm getJoinAlgorithmFromContextValue(final Object object) + { + final String s = QueryContexts.getAsString( + CTX_SQL_JOIN_ALGORITHM, + object, + DEFAULT_SQL_JOIN_ALGORITHM.toString() + ); + + try { + return JoinAlgorithm.fromString(s); + } + catch (IllegalArgumentException e) { + throw QueryContexts.badValueException( + CTX_SQL_JOIN_ALGORITHM, + StringUtils.format("one of %s", Arrays.toString(JoinAlgorithm.values())), + object + ); + } + } } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java index 32ca488e923a..eb748780c9b7 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java @@ -41,6 +41,7 @@ import org.apache.druid.query.policy.PolicyEnforcer; import org.apache.druid.segment.join.JoinableFactoryWrapper; import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.NoopEscalator; @@ -49,6 +50,7 @@ import org.apache.druid.sql.calcite.planner.convertlet.DruidConvertletTable; import org.apache.druid.sql.calcite.run.SqlEngine; import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchemaName; import org.apache.druid.sql.hook.DruidHook; import org.apache.druid.sql.hook.DruidHookDispatcher; @@ -61,7 +63,7 @@ public class PlannerFactory extends PlannerToolbox { @Inject public PlannerFactory( - final DruidSchemaCatalog rootSchema, + final DruidSchemaCatalogProvider rootSchemaProvider, final DruidOperatorTable operatorTable, final ExprMacroTable macroTable, final PlannerConfig plannerConfig, @@ -81,7 +83,7 @@ public PlannerFactory( macroTable, jsonMapper, plannerConfig, - rootSchema, + rootSchemaProvider, joinableFactoryWrapper, catalog, druidSchemaName, @@ -111,6 +113,7 @@ public DruidPlanner createPlanner( final SqlEngine engine, final String sql, final SqlNode sqlNode, + final AuthenticationResult authenticationResult, final Set authContextKeys, final Map queryContext, final PlannerHook hook @@ -121,13 +124,14 @@ public DruidPlanner createPlanner( sql, sqlNode, engine, + authenticationResult, authContextKeys, queryContext, hook ); context.dispatchHook(DruidHook.SQL, sql); - return new DruidPlanner(buildFrameworkConfig(context), context, engine, hook); + return new DruidPlanner(buildFrameworkConfig(context.getRootSchema(), context), context, engine, hook); } /** @@ -146,14 +150,13 @@ public DruidPlanner createPlannerForTesting( engine, sql, statementAndSetContext.getMainStatement(), + NoopEscalator.getInstance().createEscalatedAuthenticationResult(), Set.copyOf(queryContext.keySet()), statementAndSetContext.getSetContext().isEmpty() ? queryContext : QueryContexts.override(queryContext, statementAndSetContext.getSetContext()), null ); - thePlanner.getPlannerContext() - .setAuthenticationResult(NoopEscalator.getInstance().createEscalatedAuthenticationResult()); thePlanner.validate(); thePlanner.authorize(ra -> AuthorizationResult.ALLOW_NO_RESTRICTION, ImmutableSet.of()); return thePlanner; @@ -164,7 +167,10 @@ public AuthorizerMapper getAuthorizerMapper() return authorizerMapper; } - private FrameworkConfig buildFrameworkConfig(PlannerContext plannerContext) + private FrameworkConfig buildFrameworkConfig( + DruidSchemaCatalog rootSchema, + PlannerContext plannerContext + ) { final SqlToRelConverter.Config sqlToRelConverterConfig = SqlToRelConverter .config() @@ -175,7 +181,7 @@ private FrameworkConfig buildFrameworkConfig(PlannerContext plannerContext) plannerContext.queryContext().getInSubQueryThreshold() ); - Frameworks.ConfigBuilder frameworkConfigBuilder = Frameworks + final Frameworks.ConfigBuilder frameworkConfigBuilder = Frameworks .newConfigBuilder() .parserConfig(DruidSqlParser.PARSER_CONFIG) .traitDefs(ConventionTraitDef.INSTANCE, RelCollationTraitDef.INSTANCE) diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerToolbox.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerToolbox.java index 17887afd06a5..5f857330cef9 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerToolbox.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerToolbox.java @@ -26,7 +26,7 @@ import org.apache.druid.segment.join.JoinableFactoryWrapper; import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthorizerMapper; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.hook.DruidHookDispatcher; public class PlannerToolbox @@ -36,7 +36,7 @@ public class PlannerToolbox protected final JoinableFactoryWrapper joinableFactoryWrapper; protected final ObjectMapper jsonMapper; protected final PlannerConfig plannerConfig; - protected final DruidSchemaCatalog rootSchema; + protected final DruidSchemaCatalogProvider rootSchemaProvider; protected final CatalogResolver catalog; protected final String druidSchemaName; protected final CalciteRulesManager calciteRuleManager; @@ -50,7 +50,7 @@ public PlannerToolbox( final ExprMacroTable macroTable, final ObjectMapper jsonMapper, final PlannerConfig plannerConfig, - final DruidSchemaCatalog rootSchema, + final DruidSchemaCatalogProvider rootSchemaProvider, final JoinableFactoryWrapper joinableFactoryWrapper, final CatalogResolver catalog, final String druidSchemaName, @@ -65,7 +65,7 @@ public PlannerToolbox( this.macroTable = macroTable; this.jsonMapper = jsonMapper; this.plannerConfig = Preconditions.checkNotNull(plannerConfig, "plannerConfig"); - this.rootSchema = rootSchema; + this.rootSchemaProvider = rootSchemaProvider; this.joinableFactoryWrapper = joinableFactoryWrapper; this.catalog = catalog; this.druidSchemaName = druidSchemaName; @@ -91,11 +91,6 @@ public ObjectMapper jsonMapper() return jsonMapper; } - public DruidSchemaCatalog rootSchema() - { - return rootSchema; - } - public JoinableFactoryWrapper joinableFactoryWrapper() { return joinableFactoryWrapper; @@ -111,11 +106,6 @@ public String druidSchemaName() return druidSchemaName; } - public CalciteRulesManager calciteRuleManager() - { - return calciteRuleManager; - } - public PlannerConfig plannerConfig() { return plannerConfig; diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java index 5853e19c0b50..77a5d01dcaea 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java @@ -401,6 +401,7 @@ protected PlannerResult planExplanation( .stream() .map(ResourceAction::getResource) .sorted(Comparator.comparing(Resource::getName)) + .distinct() .collect(Collectors.toList()); resourcesString = plannerContext.getJsonMapper().writeValueAsString(resources); } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/SqlResourceCollectorShuttle.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/SqlResourceCollectorShuttle.java index 4300c7d574b7..e2c5739dae80 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/SqlResourceCollectorShuttle.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/SqlResourceCollectorShuttle.java @@ -31,21 +31,27 @@ import org.apache.druid.server.security.Action; import org.apache.druid.server.security.Resource; import org.apache.druid.server.security.ResourceAction; -import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.expression.AuthorizableOperator; import org.apache.druid.sql.calcite.schema.NamedLookupSchema; +import org.apache.druid.sql.calcite.view.ViewManager; import java.util.HashSet; import java.util.List; import java.util.Set; /** - * Walks an {@link SqlNode} to collect a set of {@link Resource} for {@link ResourceType#DATASOURCE} and - * {@link ResourceType#VIEW} to use for authorization during query planning. + * Walks an {@link SqlNode} to collect a set of {@link Resource} to use for authorization during query planning. + * Two mechanisms are used to gather resources: * - * It works by looking for {@link SqlIdentifier} which correspond to a {@link IdentifierNamespace}, where + *

First, look for {@link SqlIdentifier} which correspond to a {@link IdentifierNamespace}, where * {@link SqlValidatorNamespace} is calcite-speak for sources of data and {@link IdentifierNamespace} specifically are - * namespaces which are identified by a single variable, e.g. table names. + * namespaces which are identified by a single variable, e.g. table names. These are translated into resources + * using {@link PlannerContext#getSchemaResourceType}. + * + *

Second, look for {@link AuthorizableOperator} and call {@link AuthorizableOperator#computeResources}. + * + *

Resources are collected prior to view expansion, which makes views a security boundary. Resources accessed + * solely through views are exempt from authorization. See {@link ViewManager} for details on the security model. */ public class SqlResourceCollectorShuttle extends SqlShuttle { diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidCalciteSchemaModule.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidCalciteSchemaModule.java index 14e1fc068824..6272545b2689 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidCalciteSchemaModule.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidCalciteSchemaModule.java @@ -21,9 +21,6 @@ import com.google.inject.Binder; import com.google.inject.Module; -import com.google.inject.Provides; -import com.google.inject.name.Named; -import com.google.inject.name.Names; import org.apache.druid.guice.LazySingleton; import org.apache.druid.guice.LifecycleModule; import org.apache.druid.sql.guice.SqlBindings; @@ -34,40 +31,20 @@ public class DruidCalciteSchemaModule implements Module { private static final String DRUID_SCHEMA_NAME = "druid"; - private static final String INFORMATION_SCHEMA_NAME = "INFORMATION_SCHEMA"; - static final String INCOMPLETE_SCHEMA = "INCOMPLETE_SCHEMA"; @Override public void configure(Binder binder) { binder.bind(String.class).annotatedWith(DruidSchemaName.class).toInstance(DRUID_SCHEMA_NAME); - - // Should only be used by the information schema - binder.bind(DruidSchemaCatalog.class) - .annotatedWith(Names.named(INCOMPLETE_SCHEMA)) - .toProvider(RootSchemaProvider.class) - .in(LazySingleton.class); + binder.bind(DruidSchemaCatalogProvider.class).to(DruidSchemaCatalogProviderImpl.class).in(LazySingleton.class); // BrokerSegmentMetadataCache needs to listen to changes for incoming segments LifecycleModule.register(binder, BrokerSegmentMetadataCache.class); - binder.bind(DruidSchema.class).in(LazySingleton.class); - binder.bind(SystemSchema.class).in(LazySingleton.class); - binder.bind(InformationSchema.class).in(LazySingleton.class); - binder.bind(LookupSchema.class).in(LazySingleton.class); - // Binder to inject different schema to Calcite - SqlBindings.addSchema(binder, NamedDruidSchema.class); - SqlBindings.addSchema(binder, NamedSystemSchema.class); + SqlBindings.addSchemaProvider(binder, DruidSchemaProvider.class); + SqlBindings.addSchemaProvider(binder, SystemSchemaProvider.class); + SqlBindings.addSchemaProvider(binder, ViewSchemaProvider.class); SqlBindings.addSchema(binder, NamedLookupSchema.class); - SqlBindings.addSchema(binder, NamedViewSchema.class); - } - - @Provides - @LazySingleton - private DruidSchemaCatalog getRootSchema(@Named(INCOMPLETE_SCHEMA) DruidSchemaCatalog rootSchema, InformationSchema informationSchema) - { - rootSchema.getRootSchema().add(INFORMATION_SCHEMA_NAME, informationSchema); - return rootSchema; } } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchema.java index 96cd166b1565..6048acf043f2 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchema.java @@ -19,13 +19,15 @@ package org.apache.druid.sql.calcite.schema; +import com.google.common.base.Preconditions; import org.apache.calcite.schema.Table; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.planner.CatalogResolver; import org.apache.druid.sql.calcite.table.DatasourceTable; import org.apache.druid.sql.calcite.table.DruidTable; -import javax.inject.Inject; - import java.util.Set; public class DruidSchema extends AbstractTableSchema @@ -33,12 +35,17 @@ public class DruidSchema extends AbstractTableSchema private final BrokerSegmentMetadataCache segmentMetadataCache; private final DruidSchemaManager druidSchemaManager; private final CatalogResolver catalogResolver; + private final AuthorizerMapper authorizerMapper; + private final AuthenticationResult authenticationResult; + private final boolean authorizeTableVisibility; - @Inject public DruidSchema( final BrokerSegmentMetadataCache segmentMetadataCache, final DruidSchemaManager druidSchemaManager, - final CatalogResolver catalogResolver + final CatalogResolver catalogResolver, + final AuthorizerMapper authorizerMapper, + final AuthenticationResult authenticationResult, + final boolean authorizeTableVisibility ) { this.segmentMetadataCache = segmentMetadataCache; @@ -48,16 +55,20 @@ public DruidSchema( } else { this.druidSchemaManager = null; } - } - - protected BrokerSegmentMetadataCache cache() - { - return segmentMetadataCache; + this.authorizerMapper = authorizerMapper; + this.authenticationResult = Preconditions.checkNotNull(authenticationResult, "authenticationResult"); + this.authorizeTableVisibility = authorizeTableVisibility; } @Override public Table getTable(String name) { + if (authorizeTableVisibility + && !SchemaUtils.isTableVisible(authorizerMapper, authenticationResult, name, _ -> ResourceType.DATASOURCE)) { + // Do not return tables that are not supposed to be visible in this schema. + return null; + } + DruidTable schemaMgrTable = null; DruidTable catalogTable = catalogResolver.resolveDatasource(name, null); if (catalogTable == null && druidSchemaManager != null) { @@ -74,10 +85,22 @@ public Table getTable(String name) @Override public Set getTableNames() { + final Set allTableNames; if (druidSchemaManager != null) { - return druidSchemaManager.getTableNames(segmentMetadataCache); + allTableNames = druidSchemaManager.getTableNames(segmentMetadataCache); + } else { + allTableNames = catalogResolver.getTableNames(segmentMetadataCache.getDatasourceNames()); + } + + if (authorizeTableVisibility) { + return SchemaUtils.filterVisibleTables( + authorizerMapper, + authenticationResult, + allTableNames, + _ -> ResourceType.DATASOURCE + ); } else { - return catalogResolver.getTableNames(segmentMetadataCache.getDatasourceNames()); + return allTableNames; } } } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalog.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalog.java index d81426af0868..3b169b7d0987 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalog.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalog.java @@ -19,7 +19,9 @@ package org.apache.druid.sql.calcite.schema; +import com.google.common.base.Preconditions; import org.apache.calcite.schema.SchemaPlus; +import org.apache.druid.server.security.Resource; import javax.annotation.Nullable; import java.util.Map; @@ -37,9 +39,9 @@ * planning and execution. * * {@link #namedSchemas} contains all {@link NamedSchema}, which should be everything except {@link InformationSchema}. - * These are used primarily for {@link #getResourceType(String, String)}, which given the name of a table or function + * These are used primarily for {@link #getResource(String, String)}, which given the name of a table or function * that belongs to some {@link NamedSchema}, lookup the most appropriate value to use for - * {@link org.apache.druid.server.security.Resource#getType()} to use for authorization. + * {@link Resource#getType()} to use for authorization. */ public class DruidSchemaCatalog { @@ -48,11 +50,11 @@ public class DruidSchemaCatalog public DruidSchemaCatalog( final SchemaPlus rootSchema, - final Map schemas + final Map namedSchemas ) { - this.rootSchema = rootSchema; - this.namedSchemas = schemas; + this.rootSchema = Preconditions.checkNotNull(rootSchema, "rootSchema"); + this.namedSchemas = Preconditions.checkNotNull(namedSchemas, "namedSchemas"); } /** @@ -63,22 +65,6 @@ public SchemaPlus getRootSchema() return rootSchema; } - /** - * Get all {@link NamedSchema} which belong to the Druid catalog - */ - public Map getNamedSchemas() - { - return namedSchemas; - } - - /** - * Get a {@link NamedSchema} by {@link NamedSchema#getSchemaName()} - */ - public NamedSchema getNamedSchema(String schemaName) - { - return namedSchemas.get(schemaName); - } - /** * Get a specific {@link SchemaPlus} by {@link NamedSchema#getSchemaName()} */ @@ -97,7 +83,7 @@ public Set getSubSchemaNames() /** * Given the name of a {@link NamedSchema} and the name of a table or function that belongs to that schema, return - * the appropriate value to use for {@link org.apache.druid.server.security.Resource#getType()} during authorization + * the appropriate value to use for {@link Resource#getType()} during authorization */ @Nullable public String getResourceType(String schema, String resourceName) diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalogProvider.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalogProvider.java new file mode 100644 index 000000000000..f2bfa6ac3e9e --- /dev/null +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalogProvider.java @@ -0,0 +1,40 @@ +/* + * 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.druid.sql.calcite.schema; + +import org.apache.druid.server.security.AuthenticationResult; + +/** + * Provides root schema wrappers in the form of {@link DruidSchemaCatalog} instances. + */ +public interface DruidSchemaCatalogProvider +{ + /** + * Constructs a root schema for the provided user. This root schema contains the objects that are visible to + * the provided user. The user is not necessarily authorized to perform all operations, or even any operations, + * on these objects. Authorization must be checked separately. + */ + DruidSchemaCatalog createRootSchema(AuthenticationResult authenticationResult); + + /** + * Constructs an escalated root schema. Typically this contains all objects. + */ + DruidSchemaCatalog createEscalatedRootSchema(); +} diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalogProviderImpl.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalogProviderImpl.java new file mode 100644 index 000000000000..a88702128a1b --- /dev/null +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaCatalogProviderImpl.java @@ -0,0 +1,115 @@ +/* + * 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.druid.sql.calcite.schema; + +import com.google.inject.Inject; +import org.apache.calcite.jdbc.CalciteSchema; +import org.apache.calcite.schema.SchemaPlus; +import org.apache.druid.guice.LazySingleton; +import org.apache.druid.java.util.common.ISE; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Escalator; +import org.apache.druid.sql.calcite.planner.DruidOperatorTable; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +@LazySingleton +public class DruidSchemaCatalogProviderImpl implements DruidSchemaCatalogProvider +{ + private static final String INFORMATION_SCHEMA_NAME = "INFORMATION_SCHEMA"; + + private final Set namedSchemas; + private final Set schemaProviders; + private final DruidOperatorTable operatorTable; + private final AuthorizerMapper authorizerMapper; + private final Escalator escalator; + + @Inject + public DruidSchemaCatalogProviderImpl( + Set namedSchemas, + Set schemaProviders, + DruidOperatorTable operatorTable, + AuthorizerMapper authorizerMapper, + Escalator escalator + ) + { + this.namedSchemas = namedSchemas; + this.schemaProviders = schemaProviders; + this.operatorTable = operatorTable; + this.authorizerMapper = authorizerMapper; + this.escalator = escalator; + } + + @Override + public DruidSchemaCatalog createRootSchema(final AuthenticationResult authenticationResult) + { + // Metadata schema is disabled because it is not needed. Caching is disabled because we want to avoid + // materializing every table, as Calcite's caching schema would do. + final SchemaPlus rootSchema = CalciteSchema.createRootSchema(false, false).plus(); + final Map allSchemas = new TreeMap<>(); + + for (NamedSchema schema : namedSchemas) { + if (allSchemas.putIfAbsent(schema.getSchemaName(), schema) != null) { + throw new ISE("Schema name conflict for[%s]", schema.getSchemaName()); + } + } + + for (SchemaProvider schemaProvider : schemaProviders) { + for (NamedSchema schema : schemaProvider.getSchemas(authenticationResult)) { + if (allSchemas.putIfAbsent(schema.getSchemaName(), schema) != null) { + throw new ISE("Schema name conflict for[%s]", schema.getSchemaName()); + } + } + } + + if (allSchemas.containsKey(INFORMATION_SCHEMA_NAME)) { + throw new ISE("Cannot have schema named[%s]", INFORMATION_SCHEMA_NAME); + } + + // Add allSchemas to the rootSchema. + for (final NamedSchema namedSchema : allSchemas.values()) { + rootSchema.add(namedSchema.getSchemaName(), namedSchema.getSchema()); + } + + final DruidSchemaCatalog schemaCatalog = new DruidSchemaCatalog(rootSchema, allSchemas); + + // One more schema to add: INFORMATION_SCHEMA. + rootSchema.add( + INFORMATION_SCHEMA_NAME, + new InformationSchema( + schemaCatalog, + operatorTable, + authorizerMapper, + authenticationResult + ) + ); + + return schemaCatalog; + } + + @Override + public DruidSchemaCatalog createEscalatedRootSchema() + { + return createRootSchema(escalator.createEscalatedAuthenticationResult()); + } +} diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaProvider.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaProvider.java new file mode 100644 index 000000000000..a8b1b68c3a8a --- /dev/null +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/DruidSchemaProvider.java @@ -0,0 +1,89 @@ +/* + * 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.druid.sql.calcite.schema; + +import org.apache.druid.guice.LazySingleton; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.sql.calcite.planner.CatalogResolver; +import org.apache.druid.sql.calcite.planner.PlannerConfig; + +import javax.inject.Inject; +import java.util.List; + +@LazySingleton +public class DruidSchemaProvider implements SchemaProvider +{ + private final String schemaName; + private final BrokerSegmentMetadataCache segmentMetadataCache; + private final DruidSchemaManager druidSchemaManager; + private final CatalogResolver catalogResolver; + private final PlannerConfig plannerConfig; + private final AuthorizerMapper authorizerMapper; + + @Inject + public DruidSchemaProvider( + @DruidSchemaName final String schemaName, + final BrokerSegmentMetadataCache segmentMetadataCache, + final DruidSchemaManager druidSchemaManager, + final CatalogResolver catalogResolver, + final PlannerConfig plannerConfig, + final AuthorizerMapper authorizerMapper + ) + { + this.schemaName = schemaName; + this.segmentMetadataCache = segmentMetadataCache; + this.catalogResolver = catalogResolver; + this.plannerConfig = plannerConfig; + this.authorizerMapper = authorizerMapper; + if (druidSchemaManager != null && !(druidSchemaManager instanceof NoopDruidSchemaManager)) { + this.druidSchemaManager = druidSchemaManager; + } else { + this.druidSchemaManager = null; + } + } + + @Override + public List getSchemas(AuthenticationResult authenticationResult) + { + return List.of( + new NamedDruidSchema( + new DruidSchema( + segmentMetadataCache, + druidSchemaManager, + catalogResolver, + authorizerMapper, + authenticationResult, + plannerConfig.isAuthorizeTableVisibility() + ), + schemaName + ) + ); + } + + /** + * Returns the underlying metadata cache used by this instance. Not filtered based on authorization, so this + * should only be used by code that is applying authorization filters to the table list some other way. + */ + public BrokerSegmentMetadataCache getSegmentMetadataCache() + { + return segmentMetadataCache; + } +} diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/InformationSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/InformationSchema.java index 94e11de1c720..e4109a6a4b0a 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/InformationSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/InformationSchema.java @@ -24,17 +24,13 @@ import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; -import com.google.inject.Inject; -import com.google.inject.name.Named; import org.apache.calcite.DataContext; import org.apache.calcite.linq4j.Enumerable; import org.apache.calcite.linq4j.Linq4j; import org.apache.calcite.rel.type.RelDataType; import org.apache.calcite.rel.type.RelDataTypeFactory; import org.apache.calcite.rel.type.RelDataTypeField; -import org.apache.calcite.rel.type.RelDataTypeSystem; import org.apache.calcite.schema.ScannableTable; import org.apache.calcite.schema.SchemaPlus; import org.apache.calcite.schema.Statistic; @@ -43,19 +39,15 @@ import org.apache.calcite.schema.TableMacro; import org.apache.calcite.schema.impl.AbstractSchema; import org.apache.calcite.schema.impl.AbstractTable; +import org.apache.calcite.schema.lookup.LikePattern; import org.apache.calcite.sql.SqlOperator; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.druid.java.util.emitter.EmittingLogger; -import org.apache.druid.server.security.Action; import org.apache.druid.server.security.AuthenticationResult; -import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; -import org.apache.druid.server.security.Resource; -import org.apache.druid.server.security.ResourceAction; import org.apache.druid.sql.calcite.planner.Calcites; import org.apache.druid.sql.calcite.planner.DruidOperatorTable; import org.apache.druid.sql.calcite.planner.DruidTypeSystem; -import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.table.DruidTable; import org.apache.druid.sql.calcite.table.RowSignatures; @@ -71,9 +63,9 @@ public class InformationSchema extends AbstractSchema { private static final EmittingLogger log = new EmittingLogger(InformationSchema.class); - private static final String CATALOG_NAME = "druid"; - private static final String INFORMATION_SCHEMA_NAME = "INFORMATION_SCHEMA"; + public static final String INFORMATION_SCHEMA_NAME = "INFORMATION_SCHEMA"; + private static final String CATALOG_NAME = "druid"; private static final String SCHEMATA_TABLE = "SCHEMATA"; private static final String TABLES_TABLE = "TABLES"; private static final String COLUMNS_TABLE = "COLUMNS"; @@ -146,30 +138,31 @@ public RelDataType build() .add("IS_AGGREGATOR", SqlTypeName.VARCHAR) .add("SIGNATURES", SqlTypeName.VARCHAR, true) .build(); - private static final RelDataTypeSystem TYPE_SYSTEM = RelDataTypeSystem.DEFAULT; private static final String INFO_TRUE = "YES"; private static final String INFO_FALSE = "NO"; private final DruidSchemaCatalog rootSchema; - private final Map tableMap; private final AuthorizerMapper authorizerMapper; + private final AuthenticationResult authenticationResult; + private final Map tableMap; - @Inject public InformationSchema( - @Named(DruidCalciteSchemaModule.INCOMPLETE_SCHEMA) final DruidSchemaCatalog rootSchema, + final DruidSchemaCatalog rootSchema, + final DruidOperatorTable operatorTable, final AuthorizerMapper authorizerMapper, - final DruidOperatorTable operatorTable + final AuthenticationResult authenticationResult ) { this.rootSchema = Preconditions.checkNotNull(rootSchema, "rootSchema"); + this.authorizerMapper = Preconditions.checkNotNull(authorizerMapper, "authorizerMapper"); + this.authenticationResult = Preconditions.checkNotNull(authenticationResult, "authenticationResult"); this.tableMap = ImmutableMap.of( SCHEMATA_TABLE, new SchemataTable(), TABLES_TABLE, new TablesTable(), COLUMNS_TABLE, new ColumnsTable(), ROUTINES_TABLE, new RoutinesTable(operatorTable) ); - this.authorizerMapper = authorizerMapper; } @Override @@ -178,6 +171,16 @@ protected Map getTableMap() return tableMap; } + private Set getVisibleNames(final Iterable allNames, final Function resourceTypeFn) + { + return SchemaUtils.filterVisibleTables( + authorizerMapper, + authenticationResult, + allNames, + resourceTypeFn + ); + } + class SchemataTable extends AbstractTable implements ScannableTable { @Override @@ -185,22 +188,25 @@ public Enumerable scan(final DataContext root) { final FluentIterable results = FluentIterable .from(rootSchema.getSubSchemaNames()) - .transform( - new Function<>() - { - @Override - public Object[] apply(final String schemaName) - { - final SchemaPlus subSchema = rootSchema.getSubSchema(schemaName); - return new Object[]{ - CATALOG_NAME, // CATALOG_NAME - subSchema.getName(), // SCHEMA_NAME - null, // SCHEMA_OWNER - null, // DEFAULT_CHARACTER_SET_CATALOG - null, // DEFAULT_CHARACTER_SET_SCHEMA - null, // DEFAULT_CHARACTER_SET_NAME - null // SQL_PATH - }; + .transformAndConcat( + schemaName -> { + final SchemaPlus subSchema = rootSchema.getSubSchema(schemaName); + if (subSchema.tables().getNames(LikePattern.any()).isEmpty() + && subSchema.getFunctionNames().isEmpty()) { + // Skip schemata that have no tables or functions. + return List.of(); + } else { + return List.of( + new Object[]{ + CATALOG_NAME, // CATALOG_NAME + subSchema.getName(), // SCHEMA_NAME + null, // SCHEMA_OWNER + null, // DEFAULT_CHARACTER_SET_CATALOG + null, // DEFAULT_CHARACTER_SET_SCHEMA + null, // DEFAULT_CHARACTER_SET_NAME + null // SQL_PATH + } + ); } } ); @@ -241,23 +247,18 @@ public Enumerable scan(final DataContext root) public Iterable apply(final String schemaName) { final SchemaPlus subSchema = rootSchema.getSubSchema(schemaName); - - final AuthenticationResult authenticationResult = - (AuthenticationResult) root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT); - - final Set authorizedTableNames = getAuthorizedTableNamesFromSubSchema( - subSchema, - authenticationResult + final Set tableNames = getVisibleNames( + subSchema.tables().getNames(LikePattern.any()), + tableName -> rootSchema.getResourceType(subSchema.getName(), tableName) ); - - final Set authorizedFunctionNames = getAuthorizedFunctionNamesFromSubSchema( - subSchema, - authenticationResult + final Set functionNames = getVisibleNames( + subSchema.getFunctionNames(), + tableName -> rootSchema.getResourceType(subSchema.getName(), tableName) ); return Iterables.filter( Iterables.concat( - FluentIterable.from(authorizedTableNames).transform( + FluentIterable.from(tableNames).transform( tableName -> { final Table table = subSchema.getTable(tableName); final boolean isJoinable; @@ -281,7 +282,7 @@ public Iterable apply(final String schemaName) }; } ), - FluentIterable.from(authorizedFunctionNames).transform( + FluentIterable.from(functionNames).transform( new Function<>() { @Override @@ -346,24 +347,19 @@ public Iterable apply(final String schemaName) { final SchemaPlus subSchema = rootSchema.getSubSchema(schemaName); final RelDataTypeFactory typeFactory = root.getTypeFactory(); - - final AuthenticationResult authenticationResult = - (AuthenticationResult) root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT); - - final Set authorizedTableNames = getAuthorizedTableNamesFromSubSchema( - subSchema, - authenticationResult + final Set tableNames = getVisibleNames( + subSchema.tables().getNames(LikePattern.any()), + tableName -> rootSchema.getResourceType(subSchema.getName(), tableName) ); - - final Set authorizedFunctionNames = getAuthorizedFunctionNamesFromSubSchema( - subSchema, - authenticationResult + final Set functionNames = getVisibleNames( + subSchema.getFunctionNames(), + tableName -> rootSchema.getResourceType(subSchema.getName(), tableName) ); return Iterables.concat( Iterables.filter( Iterables.concat( - FluentIterable.from(authorizedTableNames).transform( + FluentIterable.from(tableNames).transform( new Function<>() { @Override @@ -382,7 +378,7 @@ public Iterable apply(final String tableName) } } ), - FluentIterable.from(authorizedFunctionNames).transform( + FluentIterable.from(functionNames).transform( new Function<>() { @Override @@ -564,56 +560,4 @@ private static TableMacro getView(final SchemaPlus schemaPlus, final String func return null; } - - private Set getAuthorizedTableNamesFromSubSchema( - final SchemaPlus subSchema, - final AuthenticationResult authenticationResult - ) - { - return getAuthorizedNamesFromNamedSchema( - authenticationResult, - rootSchema.getNamedSchema(subSchema.getName()), - subSchema.getTableNames() - ); - } - - private Set getAuthorizedFunctionNamesFromSubSchema( - final SchemaPlus subSchema, - final AuthenticationResult authenticationResult - ) - { - return getAuthorizedNamesFromNamedSchema( - authenticationResult, - rootSchema.getNamedSchema(subSchema.getName()), - subSchema.getFunctionNames() - ); - } - - private Set getAuthorizedNamesFromNamedSchema( - final AuthenticationResult authenticationResult, - final NamedSchema schema, - final Set names - ) - { - if (schema == null) { - // for schemas with no resource type, or that are not named schemas, we don't filter anything - return names; - } - return ImmutableSet.copyOf( - AuthorizationUtils.filterAuthorizedResources( - authenticationResult, - names, - name -> { - final String resourseType = schema.getSchemaResourceType(name); - if (resourseType == null) { - return Collections.emptyList(); - } - return Collections.singletonList( - new ResourceAction(new Resource(name, resourseType), Action.READ) - ); - }, - authorizerMapper - ) - ); - } } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/LookupSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/LookupSchema.java index b8453ba551c6..1f617600b608 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/LookupSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/LookupSchema.java @@ -23,6 +23,7 @@ import com.google.inject.Inject; import org.apache.calcite.schema.Table; import org.apache.calcite.schema.impl.AbstractSchema; +import org.apache.druid.guice.LazySingleton; import org.apache.druid.query.LookupDataSource; import org.apache.druid.query.lookup.LookupExtractorFactoryContainerProvider; import org.apache.druid.segment.column.ColumnType; @@ -35,6 +36,7 @@ /** * Creates the "lookup" schema in Druid SQL, composed of all available {@link LookupDataSource}. */ +@LazySingleton public class LookupSchema extends AbstractSchema { private static final RowSignature ROW_SIGNATURE = diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedDruidSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedDruidSchema.java index 2e8de70c2feb..bc0a791bbd90 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedDruidSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedDruidSchema.java @@ -19,7 +19,6 @@ package org.apache.druid.sql.calcite.schema; -import com.google.inject.Inject; import org.apache.calcite.schema.Schema; import org.apache.druid.server.security.ResourceType; @@ -31,7 +30,6 @@ public class NamedDruidSchema implements NamedSchema private final DruidSchema druidSchema; private final String druidSchemaName; - @Inject public NamedDruidSchema(DruidSchema druidSchema, @DruidSchemaName String druidSchemaName) { this.druidSchema = druidSchema; diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedLookupSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedLookupSchema.java index eb91949126b7..e99c2ffab906 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedLookupSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedLookupSchema.java @@ -21,10 +21,12 @@ import com.google.inject.Inject; import org.apache.calcite.schema.Schema; +import org.apache.druid.guice.LazySingleton; /** * The schema for Druid lookup tables to be accessible via SQL. */ +@LazySingleton public class NamedLookupSchema implements NamedSchema { public static final String NAME = "lookup"; diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedSystemSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedSystemSchema.java index 65bd3adc5040..be206b9caa26 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedSystemSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedSystemSchema.java @@ -19,7 +19,6 @@ package org.apache.druid.sql.calcite.schema; -import com.google.inject.Inject; import org.apache.calcite.schema.Schema; import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.planner.PlannerConfig; @@ -36,7 +35,6 @@ public class NamedSystemSchema implements NamedSchema private final SystemSchema systemSchema; private final PlannerConfig plannerConfig; - @Inject public NamedSystemSchema(PlannerConfig plannerConfig, SystemSchema systemSchema) { this.plannerConfig = plannerConfig; diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedViewSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedViewSchema.java index fb6ae9cae28d..826d25ec2483 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedViewSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/NamedViewSchema.java @@ -19,7 +19,6 @@ package org.apache.druid.sql.calcite.schema; -import com.google.inject.Inject; import org.apache.calcite.schema.Schema; import org.apache.druid.server.security.ResourceType; @@ -28,7 +27,6 @@ public class NamedViewSchema implements NamedSchema public static final String NAME = "view"; private final ViewSchema viewSchema; - @Inject public NamedViewSchema(ViewSchema viewSchema) { this.viewSchema = viewSchema; diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/RootSchemaProvider.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/RootSchemaProvider.java deleted file mode 100644 index f7f4f660e23c..000000000000 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/RootSchemaProvider.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.druid.sql.calcite.schema; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import com.google.inject.Inject; -import com.google.inject.Provider; -import org.apache.calcite.jdbc.CalciteSchema; -import org.apache.calcite.schema.SchemaPlus; -import org.apache.druid.java.util.common.ISE; - -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Provides the RootSchema for Calcite with - * - metadata schema disabled because it's not needed - * - caching disabled because Druid's caching is better. - * - * All the provided schema are added to the rootSchema. - */ -public class RootSchemaProvider implements Provider -{ - private final Set namedSchemas; - private final Map schemasByName; - - @Inject - RootSchemaProvider(Set namedSchemas) - { - this.namedSchemas = namedSchemas; - schemasByName = Maps.newHashMapWithExpectedSize(namedSchemas.size()); - for (NamedSchema schema : namedSchemas) { - if (schemasByName.containsKey(schema.getSchemaName())) { - throw new ISE( - "Found multiple schemas registered to the same name. The list of registered schemas are %s", - namedSchemas.stream().map(NamedSchema::getSchemaName).collect(Collectors.toList()) - ); - } - schemasByName.put(schema.getSchemaName(), schema); - } - } - - @Override - public DruidSchemaCatalog get() - { - final SchemaPlus rootSchema = CalciteSchema.createRootSchema(false, false).plus(); - for (NamedSchema schema : namedSchemas) { - rootSchema.add(schema.getSchemaName(), schema.getSchema()); - } - return new DruidSchemaCatalog(rootSchema, ImmutableMap.copyOf(schemasByName)); - } -} diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SchemaProvider.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SchemaProvider.java new file mode 100644 index 000000000000..5f99e9b30b96 --- /dev/null +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SchemaProvider.java @@ -0,0 +1,46 @@ +/* + * 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.druid.sql.calcite.schema; + +import com.google.inject.Binder; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.sql.calcite.planner.PlannerConfig; +import org.apache.druid.sql.guice.SqlBindings; + +import java.util.List; + +/** + * Provides {@link NamedSchema} in a user-aware way. Bind with {@link SqlBindings#addSchemaProvider(Binder, Class)}. + */ +public interface SchemaProvider +{ + /** + * Return a list of {@link NamedSchema} for the provided user. These schemas contain the objects that are + * visible to the provided user. The user is not necessarily authorized to perform all operations, or even + * any operations, on these objects. Authorization must be checked separately. + * + *

Schema providers that produce authorizable tables must check the value of + * {@link PlannerConfig#isAuthorizeTableVisibility()} and use this to determine whether to place unauthorized + * tables in the returned schemas. + * + * @param authenticationResult identity of the current user + */ + List getSchemas(AuthenticationResult authenticationResult); +} diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SchemaUtils.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SchemaUtils.java new file mode 100644 index 000000000000..6e8202c8b06b --- /dev/null +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SchemaUtils.java @@ -0,0 +1,84 @@ +/* + * 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.druid.sql.calcite.schema; + +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +public class SchemaUtils +{ + private SchemaUtils() + { + // No instantiation. + } + + public static boolean isTableVisible( + final AuthorizerMapper authorizerMapper, + final AuthenticationResult authenticationResult, + final String tableName, + final Function resourceTypeFn + ) + { + return !filterVisibleTables(authorizerMapper, authenticationResult, Set.of(tableName), resourceTypeFn).isEmpty(); + } + + public static Set filterVisibleTables( + final AuthorizerMapper authorizerMapper, + final AuthenticationResult authenticationResult, + final Iterable tableNames, + final Function resourceTypeFn + ) + { + final Set visibleNames = new LinkedHashSet<>(); + final Set authorizableResources = new LinkedHashSet<>(); + + for (final String tableName : tableNames) { + final String resourceType = resourceTypeFn.apply(tableName); + if (resourceType == null) { + // No ResourceType means this name does not need authorization. It's always visible. + visibleNames.add(tableName); + } else { + authorizableResources.add(new Resource(tableName, resourceType)); + } + } + + final Iterable authorizedResources = AuthorizationUtils.filterAuthorizedResources( + authenticationResult, + authorizableResources, + resource -> List.of(new ResourceAction(resource, Action.READ)), + authorizerMapper + ); + + for (final Resource resource : authorizedResources) { + visibleNames.add(resource.getName()); + } + + return visibleNames; + } +} diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java index 56d6bb593d3a..8ccbcf19c815 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java @@ -22,12 +22,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Function; -import com.google.common.base.Preconditions; import com.google.common.collect.FluentIterable; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; -import com.google.inject.Inject; import com.google.inject.Provider; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; @@ -42,7 +39,6 @@ import org.apache.calcite.schema.ProjectableFilterableTable; import org.apache.calcite.schema.ScannableTable; import org.apache.calcite.schema.Table; -import org.apache.calcite.schema.impl.AbstractSchema; import org.apache.calcite.schema.impl.AbstractTable; import org.apache.druid.client.DruidServer; import org.apache.druid.client.FilteredServerInventoryView; @@ -54,7 +50,7 @@ import org.apache.druid.discovery.DiscoveryDruidNode; import org.apache.druid.discovery.DruidNodeDiscoveryProvider; import org.apache.druid.discovery.NodeRole; -import org.apache.druid.guice.annotations.EscalatedClient; +import org.apache.druid.error.DruidException; import org.apache.druid.indexer.TaskStatusPlus; import org.apache.druid.indexing.overlord.supervisor.SupervisorStatus; import org.apache.druid.java.util.common.ISE; @@ -76,8 +72,8 @@ import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.server.security.Resource; import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.planner.PlannerConfig; -import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.run.SqlEngine; import org.apache.druid.sql.calcite.table.RowSignatures; import org.apache.druid.sql.http.GetQueriesResponse; @@ -96,20 +92,19 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; -public class SystemSchema extends AbstractSchema +public class SystemSchema extends AbstractTableSchema { - private static final String SEGMENTS_TABLE = "segments"; - private static final String SERVERS_TABLE = "servers"; - private static final String SERVER_SEGMENTS_TABLE = "server_segments"; - private static final String TASKS_TABLE = "tasks"; - private static final String SUPERVISOR_TABLE = "supervisors"; - private static final String QUERIES_TABLE = "queries"; + public static final String SEGMENTS_TABLE = "segments"; + public static final String SERVERS_TABLE = "servers"; + public static final String SERVER_SEGMENTS_TABLE = "server_segments"; + public static final String TASKS_TABLE = "tasks"; + public static final String SUPERVISOR_TABLE = "supervisors"; + public static final String QUERIES_TABLE = "queries"; private static final Function> SEGMENT_STATUS_IN_CLUSTER_RA_GENERATOR = segment -> @@ -254,11 +249,23 @@ public class SystemSchema extends AbstractSchema */ private static final int[] QUERIES_PROJECT_ALL = IntStream.range(0, QUERIES_SIGNATURE.size()).toArray(); - private final Map tableMap; + private final BrokerSegmentMetadataCache segmentMetadataCache; + private final MetadataSegmentView metadataView; + private final TimelineServerView serverView; + private final FilteredServerInventoryView serverInventoryView; + private final AuthorizerMapper authorizerMapper; + private final CoordinatorClient coordinatorClient; + private final OverlordClient overlordClient; + private final DruidNodeDiscoveryProvider druidNodeDiscoveryProvider; + private final ObjectMapper jsonMapper; + private final HttpClient httpClient; + private final Provider sqlEngineRegistryProvider; + private final PlannerConfig plannerConfig; + private final AuthenticationResult authenticationResult; + private final Set allTableNames; - @Inject public SystemSchema( - final DruidSchema druidSchema, + final BrokerSegmentMetadataCache segmentMetadataCache, final MetadataSegmentView metadataView, final TimelineServerView serverView, final FilteredServerInventoryView serverInventoryView, @@ -267,45 +274,109 @@ public SystemSchema( final OverlordClient overlordClient, final DruidNodeDiscoveryProvider druidNodeDiscoveryProvider, final ObjectMapper jsonMapper, - @EscalatedClient final HttpClient httpClient, + final HttpClient httpClient, final Provider sqlEngineRegistryProvider, - final PlannerConfig plannerConfig + final PlannerConfig plannerConfig, + final AuthenticationResult authenticationResult, + final Set allTableNames ) { - Preconditions.checkNotNull(serverView, "serverView"); - - final ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put(SEGMENTS_TABLE, new SegmentsTable(druidSchema, metadataView, jsonMapper, authorizerMapper)); - builder.put( - SERVERS_TABLE, - new ServersTable( - druidNodeDiscoveryProvider, - serverInventoryView, - authorizerMapper, - overlordClient, - coordinatorClient, - jsonMapper - ) - ); - builder.put(SERVER_SEGMENTS_TABLE, new ServerSegmentsTable(serverView, authorizerMapper)); - builder.put(TASKS_TABLE, new TasksTable(overlordClient, authorizerMapper)); - builder.put(SUPERVISOR_TABLE, new SupervisorsTable(overlordClient, authorizerMapper)); - builder.put( - SystemServerPropertiesTable.TABLE_NAME, - new SystemServerPropertiesTable(druidNodeDiscoveryProvider, authorizerMapper, httpClient, jsonMapper) - ); + this.segmentMetadataCache = segmentMetadataCache; + this.metadataView = metadataView; + this.serverView = serverView; + this.serverInventoryView = serverInventoryView; + this.authorizerMapper = authorizerMapper; + this.coordinatorClient = coordinatorClient; + this.overlordClient = overlordClient; + this.druidNodeDiscoveryProvider = druidNodeDiscoveryProvider; + this.jsonMapper = jsonMapper; + this.httpClient = httpClient; + this.sqlEngineRegistryProvider = sqlEngineRegistryProvider; + this.plannerConfig = plannerConfig; + this.authenticationResult = authenticationResult; + this.allTableNames = allTableNames; + } - if (plannerConfig.isEnableSysQueriesTable()) { - builder.put(QUERIES_TABLE, new QueriesTable(sqlEngineRegistryProvider, jsonMapper, authorizerMapper)); + @Override + @Nullable + public Table getTable(String name) + { + if (!isTableVisible(name)) { + return null; } - this.tableMap = builder.build(); + return switch (name) { + case SEGMENTS_TABLE -> new SegmentsTable( + segmentMetadataCache, + metadataView, + jsonMapper, + authorizerMapper, + authenticationResult + ); + case SERVERS_TABLE -> new ServersTable( + druidNodeDiscoveryProvider, + serverInventoryView, + authorizerMapper, + overlordClient, + coordinatorClient, + jsonMapper, + authenticationResult + ); + case SERVER_SEGMENTS_TABLE -> new ServerSegmentsTable(serverView, authorizerMapper, authenticationResult); + case TASKS_TABLE -> new TasksTable(overlordClient, authorizerMapper, authenticationResult); + case SUPERVISOR_TABLE -> new SupervisorsTable(overlordClient, authorizerMapper, authenticationResult); + case SystemServerPropertiesTable.TABLE_NAME -> new SystemServerPropertiesTable( + druidNodeDiscoveryProvider, + authorizerMapper, + httpClient, + jsonMapper, + authenticationResult + ); + case QUERIES_TABLE -> new QueriesTable( + sqlEngineRegistryProvider, + jsonMapper, + authorizerMapper, + authenticationResult + ); + case null, default -> throw DruidException.defensive("Unrecognized table name[%s]", name); + }; } @Override - public Map getTableMap() + public Set getTableNames() { - return tableMap; + if (plannerConfig.isAuthorizeTableVisibility()) { + return SchemaUtils.filterVisibleTables( + authorizerMapper, + authenticationResult, + allTableNames, + _ -> plannerConfig.isAuthorizeSystemTablesDirectly() ? ResourceType.SYSTEM_TABLE : null + ); + } else { + // sys table authorization is not enabled, so all sys tables are visible to all users. + return allTableNames; + } + } + + /** + * Returns whether a sys table with a particular name should be visible to the provided user. + */ + private boolean isTableVisible(final String sysTableName) + { + if (!allTableNames.contains(sysTableName)) { + // Short circuit that hides sys.queries if it is disabled server-wide. + return false; + } else if (plannerConfig.isAuthorizeTableVisibility()) { + return SchemaUtils.isTableVisible( + authorizerMapper, + authenticationResult, + sysTableName, + _ -> plannerConfig.isAuthorizeSystemTablesDirectly() ? ResourceType.SYSTEM_TABLE : null + ); + } else { + // sys table authorization is not enabled, so all sys tables are visible to all users. + return true; + } } /** @@ -315,22 +386,25 @@ static class SegmentsTable extends AbstractTable implements ProjectableFilterabl { private static final int DATASOURCE_COLUMN = SEGMENTS_SIGNATURE.indexOf("datasource"); - private final DruidSchema druidSchema; + private final BrokerSegmentMetadataCache segmentMetadataCache; private final ObjectMapper jsonMapper; private final AuthorizerMapper authorizerMapper; private final MetadataSegmentView metadataView; + private final AuthenticationResult authenticationResult; public SegmentsTable( - DruidSchema druidSchemna, + BrokerSegmentMetadataCache segmentMetadataCache, MetadataSegmentView metadataView, ObjectMapper jsonMapper, - AuthorizerMapper authorizerMapper + AuthorizerMapper authorizerMapper, + AuthenticationResult authenticationResult ) { - this.druidSchema = druidSchemna; + this.segmentMetadataCache = segmentMetadataCache; this.metadataView = metadataView; this.jsonMapper = jsonMapper; this.authorizerMapper = authorizerMapper; + this.authenticationResult = authenticationResult; } @Override @@ -352,9 +426,6 @@ public Enumerable scan( @Nullable final int[] projects ) { - // get available segments from druidSchema - final BrokerSegmentMetadataCache availableMetadataCache = druidSchema.cache(); - // Best-effort push-down of a `datasource` equality/IN filter so we scan only the matching // datasources instead of every segment in the cluster. Null => no usable filter => full scan. // The filters are intentionally left in the list, so Calcite still applies them and correctness @@ -366,18 +437,18 @@ public Enumerable scan( // datasources' segments, so avoid pre-sizing to the whole-cluster segment count (a huge, wasted allocation). final Set segmentsAlreadySeen = dataSourceFilter == null - ? Sets.newHashSetWithExpectedSize(availableMetadataCache.getTotalSegments()) + ? Sets.newHashSetWithExpectedSize(segmentMetadataCache.getTotalSegments()) : new HashSet<>(); // Get segments from metadata segment cache (if enabled in SQL planner config), else directly from // Coordinator. This may include both published and realtime segments. final Iterator metadataStoreSegments = metadataView.getSegments(dataSourceFilter); final FluentIterable publishedSegments = FluentIterable - .from(() -> getAuthorizedPublishedSegments(metadataStoreSegments, root)) + .from(() -> getAuthorizedPublishedSegments(metadataStoreSegments)) .transform(val -> { final DataSegment segment = val.getDataSegment(); final AvailableSegmentMetadata availableSegmentMetadata = - availableMetadataCache.getAvailableSegmentMetadata(segment.getDataSource(), segment.getId()); + segmentMetadataCache.getAvailableSegmentMetadata(segment.getDataSource(), segment.getId()); segmentsAlreadySeen.add(segment.getId()); long numReplicas = 0L, isAvailable = 0L; @@ -440,7 +511,7 @@ public Enumerable scan( // If druid.centralizedDatasourceSchema.enabled is set on the Coordinator, all the segments in this loop // would be covered in the previous iteration since Coordinator would return realtime segments as well. final FluentIterable availableSegments = FluentIterable - .from(() -> getAuthorizedAvailableSegments(availableMetadataCache.iterateSegmentMetadata(dataSourceFilter), root)) + .from(() -> getAuthorizedAvailableSegments(segmentMetadataCache.iterateSegmentMetadata(dataSourceFilter))) .transform(val -> { final DataSegment segment = val.getSegment(); if (segmentsAlreadySeen.contains(segment.getId())) { @@ -483,16 +554,8 @@ public Enumerable scan( .select(row -> projectSegmentsRow(row, projects, jsonMapper)); } - private Iterator getAuthorizedPublishedSegments( - Iterator it, - DataContext root - ) + private Iterator getAuthorizedPublishedSegments(Iterator it) { - final AuthenticationResult authenticationResult = (AuthenticationResult) Preconditions.checkNotNull( - root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), - "authenticationResult in dataContext" - ); - final Iterable authorizedSegments = AuthorizationUtils .filterAuthorizedResources( authenticationResult, @@ -504,15 +567,9 @@ private Iterator getAuthorizedPublishedSegments( } private Iterator getAuthorizedAvailableSegments( - Iterator availableSegmentEntries, - DataContext root + Iterator availableSegmentEntries ) { - final AuthenticationResult authenticationResult = (AuthenticationResult) Preconditions.checkNotNull( - root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), - "authenticationResult in dataContext" - ); - Function> raGenerator = segment -> Collections.singletonList( AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR.apply(segment.getSegment().getDataSource()) @@ -605,6 +662,7 @@ static class ServersTable extends AbstractTable implements ScannableTable private final OverlordClient overlordClient; private final CoordinatorClient coordinatorClient; private final ObjectMapper jsonMapper; + private final AuthenticationResult authenticationResult; public ServersTable( DruidNodeDiscoveryProvider druidNodeDiscoveryProvider, @@ -612,7 +670,8 @@ public ServersTable( AuthorizerMapper authorizerMapper, OverlordClient overlordClient, CoordinatorClient coordinatorClient, - ObjectMapper jsonMapper + ObjectMapper jsonMapper, + AuthenticationResult authenticationResult ) { this.authorizerMapper = authorizerMapper; @@ -621,6 +680,7 @@ public ServersTable( this.overlordClient = overlordClient; this.coordinatorClient = coordinatorClient; this.jsonMapper = jsonMapper; + this.authenticationResult = authenticationResult; } @Override @@ -639,10 +699,6 @@ public TableType getJdbcTableType() public Enumerable scan(DataContext root) { final Iterator druidServers = getDruidServers(druidNodeDiscoveryProvider); - final AuthenticationResult authenticationResult = (AuthenticationResult) Preconditions.checkNotNull( - root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), - "authenticationResult in dataContext" - ); checkStateReadAccessForServers(authenticationResult, authorizerMapper); String tmpCoordinatorLeader = ""; @@ -837,12 +893,18 @@ private static DruidServer toDruidServer(DiscoveryDruidNode discoveryDruidNode) static class ServerSegmentsTable extends AbstractTable implements ScannableTable { private final TimelineServerView serverView; - final AuthorizerMapper authorizerMapper; + private final AuthorizerMapper authorizerMapper; + private final AuthenticationResult authenticationResult; - public ServerSegmentsTable(TimelineServerView serverView, AuthorizerMapper authorizerMapper) + public ServerSegmentsTable( + TimelineServerView serverView, + AuthorizerMapper authorizerMapper, + AuthenticationResult authenticationResult + ) { this.serverView = serverView; this.authorizerMapper = authorizerMapper; + this.authenticationResult = authenticationResult; } @Override @@ -860,10 +922,6 @@ public TableType getJdbcTableType() @Override public Enumerable scan(DataContext root) { - final AuthenticationResult authenticationResult = (AuthenticationResult) Preconditions.checkNotNull( - root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), - "authenticationResult in dataContext" - ); checkStateReadAccessForServers(authenticationResult, authorizerMapper); final List rows = new ArrayList<>(); @@ -895,14 +953,17 @@ static class TasksTable extends AbstractTable implements ScannableTable { private final OverlordClient overlordClient; private final AuthorizerMapper authorizerMapper; + private final AuthenticationResult authenticationResult; public TasksTable( OverlordClient overlordClient, - AuthorizerMapper authorizerMapper + AuthorizerMapper authorizerMapper, + AuthenticationResult authenticationResult ) { this.overlordClient = overlordClient; this.authorizerMapper = authorizerMapper; + this.authenticationResult = authenticationResult; } @Override @@ -926,7 +987,7 @@ class TasksEnumerable extends DefaultEnumerable public TasksEnumerable(CloseableIterator tasks) { - this.it = getAuthorizedTasks(tasks, root); + this.it = getAuthorizedTasks(tasks); } @Override @@ -992,16 +1053,8 @@ public void close() return new TasksEnumerable(FutureUtils.getUnchecked(overlordClient.taskStatuses(null, null, null), true)); } - private CloseableIterator getAuthorizedTasks( - CloseableIterator it, - DataContext root - ) + private CloseableIterator getAuthorizedTasks(CloseableIterator it) { - final AuthenticationResult authenticationResult = (AuthenticationResult) Preconditions.checkNotNull( - root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), - "authenticationResult in dataContext" - ); - Function> raGenerator = task -> Collections.singletonList( AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR.apply(task.getDataSource())); @@ -1024,14 +1077,17 @@ static class SupervisorsTable extends AbstractTable implements ScannableTable { private final OverlordClient overlordClient; private final AuthorizerMapper authorizerMapper; + private final AuthenticationResult authenticationResult; public SupervisorsTable( OverlordClient overlordClient, - AuthorizerMapper authorizerMapper + AuthorizerMapper authorizerMapper, + AuthenticationResult authenticationResult ) { this.overlordClient = overlordClient; this.authorizerMapper = authorizerMapper; + this.authenticationResult = authenticationResult; } @@ -1056,7 +1112,7 @@ class SupervisorsEnumerable extends DefaultEnumerable public SupervisorsEnumerable(CloseableIterator tasks) { - this.it = getAuthorizedSupervisors(tasks, root); + this.it = getAuthorizedSupervisors(tasks); } @Override @@ -1116,16 +1172,8 @@ public void close() return new SupervisorsEnumerable(FutureUtils.getUnchecked(overlordClient.supervisorStatuses(), true)); } - private CloseableIterator getAuthorizedSupervisors( - CloseableIterator it, - DataContext root - ) + private CloseableIterator getAuthorizedSupervisors(CloseableIterator it) { - final AuthenticationResult authenticationResult = (AuthenticationResult) Preconditions.checkNotNull( - root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), - "authenticationResult in dataContext" - ); - Function> raGenerator = supervisor -> Collections.singletonList( AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR.apply(supervisor.getDataSource())); @@ -1262,16 +1310,19 @@ static class QueriesTable extends AbstractTable implements ProjectableFilterable private final Provider sqlEngineRegistryProvider; private final ObjectMapper jsonMapper; private final AuthorizerMapper authorizerMapper; + private final AuthenticationResult authenticationResult; public QueriesTable( final Provider sqlEngineRegistryProvider, final ObjectMapper jsonMapper, - final AuthorizerMapper authorizerMapper + final AuthorizerMapper authorizerMapper, + final AuthenticationResult authenticationResult ) { this.sqlEngineRegistryProvider = sqlEngineRegistryProvider; this.jsonMapper = jsonMapper; this.authorizerMapper = authorizerMapper; + this.authenticationResult = authenticationResult; } @Override @@ -1293,11 +1344,6 @@ public Enumerable scan( @Nullable final int[] projects ) { - final AuthenticationResult authenticationResult = (AuthenticationResult) Preconditions.checkNotNull( - root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), - "authenticationResult in dataContext" - ); - // Check STATE READ authorization final AuthorizationResult stateReadAuthorization = AuthorizationUtils.authorizeAllResourceActions( authenticationResult, diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchemaProvider.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchemaProvider.java new file mode 100644 index 000000000000..fe03f36203fe --- /dev/null +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchemaProvider.java @@ -0,0 +1,130 @@ +/* + * 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.druid.sql.calcite.schema; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; +import com.google.inject.Provider; +import org.apache.druid.client.FilteredServerInventoryView; +import org.apache.druid.client.TimelineServerView; +import org.apache.druid.client.coordinator.CoordinatorClient; +import org.apache.druid.discovery.DruidNodeDiscoveryProvider; +import org.apache.druid.guice.annotations.EscalatedClient; +import org.apache.druid.java.util.http.client.HttpClient; +import org.apache.druid.rpc.indexing.OverlordClient; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.sql.calcite.planner.PlannerConfig; +import org.apache.druid.sql.http.SqlEngineRegistry; + +import java.util.List; +import java.util.Set; + +public class SystemSchemaProvider implements SchemaProvider +{ + private final BrokerSegmentMetadataCache segmentMetadataCache; + private final MetadataSegmentView metadataView; + private final TimelineServerView serverView; + private final FilteredServerInventoryView serverInventoryView; + private final AuthorizerMapper authorizerMapper; + private final CoordinatorClient coordinatorClient; + private final OverlordClient overlordClient; + private final DruidNodeDiscoveryProvider druidNodeDiscoveryProvider; + private final ObjectMapper jsonMapper; + private final HttpClient httpClient; + private final Provider sqlEngineRegistryProvider; + private final PlannerConfig plannerConfig; + private final Set allTableNames; + + @Inject + public SystemSchemaProvider( + final BrokerSegmentMetadataCache segmentMetadataCache, + final MetadataSegmentView metadataView, + final TimelineServerView serverView, + final FilteredServerInventoryView serverInventoryView, + final AuthorizerMapper authorizerMapper, + final CoordinatorClient coordinatorClient, + final OverlordClient overlordClient, + final DruidNodeDiscoveryProvider druidNodeDiscoveryProvider, + final ObjectMapper jsonMapper, + @EscalatedClient final HttpClient httpClient, + final Provider sqlEngineRegistryProvider, + final PlannerConfig plannerConfig + ) + { + this.segmentMetadataCache = segmentMetadataCache; + this.metadataView = metadataView; + this.serverView = serverView; + this.serverInventoryView = serverInventoryView; + this.authorizerMapper = authorizerMapper; + this.coordinatorClient = coordinatorClient; + this.overlordClient = overlordClient; + this.druidNodeDiscoveryProvider = druidNodeDiscoveryProvider; + this.jsonMapper = jsonMapper; + this.httpClient = httpClient; + this.sqlEngineRegistryProvider = sqlEngineRegistryProvider; + this.plannerConfig = plannerConfig; + this.allTableNames = computeAllTableNames(plannerConfig); + } + + /** + * Compute the list of tables configured to exist on this server. + */ + public static Set computeAllTableNames(final PlannerConfig plannerConfig) + { + final ImmutableSet.Builder allTableNames = ImmutableSet.builder(); + allTableNames.add(SystemSchema.SEGMENTS_TABLE); + allTableNames.add(SystemSchema.SERVERS_TABLE); + allTableNames.add(SystemSchema.SERVER_SEGMENTS_TABLE); + allTableNames.add(SystemSchema.TASKS_TABLE); + allTableNames.add(SystemSchema.SUPERVISOR_TABLE); + allTableNames.add(SystemServerPropertiesTable.TABLE_NAME); + + if (plannerConfig.isEnableSysQueriesTable()) { + allTableNames.add(SystemSchema.QUERIES_TABLE); + } + + return allTableNames.build(); + } + + @Override + public List getSchemas(AuthenticationResult authenticationResult) + { + final SystemSchema systemSchema = new SystemSchema( + segmentMetadataCache, + metadataView, + serverView, + serverInventoryView, + authorizerMapper, + coordinatorClient, + overlordClient, + druidNodeDiscoveryProvider, + jsonMapper, + httpClient, + sqlEngineRegistryProvider, + plannerConfig, + authenticationResult, + allTableNames + ); + + return List.of(new NamedSystemSchema(plannerConfig, systemSchema)); + } +} diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemServerPropertiesTable.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemServerPropertiesTable.java index cdadd9d17c29..57be2383a12a 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemServerPropertiesTable.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemServerPropertiesTable.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Preconditions; import org.apache.calcite.DataContext; import org.apache.calcite.linq4j.Enumerable; import org.apache.calcite.linq4j.Linq4j; @@ -44,7 +43,6 @@ import org.apache.druid.server.DruidNode; import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.AuthorizerMapper; -import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.table.RowSignatures; import org.jboss.netty.handler.codec.http.HttpMethod; @@ -95,18 +93,21 @@ public class SystemServerPropertiesTable extends AbstractTable implements Projec private final AuthorizerMapper authorizerMapper; private final HttpClient httpClient; private final ObjectMapper jsonMapper; + private final AuthenticationResult authenticationResult; public SystemServerPropertiesTable( DruidNodeDiscoveryProvider druidNodeDiscoveryProvider, AuthorizerMapper authorizerMapper, HttpClient httpClient, - ObjectMapper jsonMapper + ObjectMapper jsonMapper, + AuthenticationResult authenticationResult ) { this.druidNodeDiscoveryProvider = druidNodeDiscoveryProvider; this.authorizerMapper = authorizerMapper; this.httpClient = httpClient; this.jsonMapper = jsonMapper; + this.authenticationResult = authenticationResult; } @Override @@ -128,10 +129,6 @@ public Enumerable scan( @Nullable final int[] projects ) { - final AuthenticationResult authenticationResult = (AuthenticationResult) Preconditions.checkNotNull( - root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), - "authenticationResult in dataContext" - ); SystemSchema.checkStateReadAccessForServers(authenticationResult, authorizerMapper); // Extract server/service_name constraints to skip fetching properties from non-matching servers. diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/ViewSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/ViewSchema.java index ca53368385d9..9cbf0ac8e2e0 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/ViewSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/ViewSchema.java @@ -20,34 +20,69 @@ package org.apache.druid.sql.calcite.schema; import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; -import com.google.inject.Inject; import org.apache.calcite.schema.Function; import org.apache.calcite.schema.impl.AbstractSchema; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.view.DruidViewMacro; import org.apache.druid.sql.calcite.view.ViewManager; import java.util.Map; +import java.util.Set; public class ViewSchema extends AbstractSchema { private final ViewManager viewManager; + private final AuthorizerMapper authorizerMapper; + private final AuthenticationResult authenticationResult; + private final boolean authorizeTableVisibility; + private final Supplier> functionMultimap = + Suppliers.memoize(this::computeFunctionMultimap); - @Inject public ViewSchema( - final ViewManager viewManager + final ViewManager viewManager, + final AuthorizerMapper authorizerMapper, + final AuthenticationResult authenticationResult, + final boolean authorizeTableVisibility ) { this.viewManager = Preconditions.checkNotNull(viewManager, "viewManager"); + this.authorizerMapper = Preconditions.checkNotNull(authorizerMapper, "authorizerMapper"); + this.authenticationResult = Preconditions.checkNotNull(authenticationResult, "authenticationResult"); + this.authorizeTableVisibility = authorizeTableVisibility; } @Override protected Multimap getFunctionMultimap() { + return functionMultimap.get(); + } + + private Multimap computeFunctionMultimap() + { + final Map viewsMap = viewManager.getViews(); + final Set visibleViews; + if (authorizeTableVisibility) { + visibleViews = SchemaUtils.filterVisibleTables( + authorizerMapper, + authenticationResult, + viewsMap.keySet(), + _ -> ResourceType.VIEW + ); + } else { + visibleViews = viewsMap.keySet(); + } + final ImmutableMultimap.Builder builder = ImmutableMultimap.builder(); - for (Map.Entry entry : viewManager.getViews().entrySet()) { - builder.put(entry); + for (Map.Entry entry : viewsMap.entrySet()) { + if (visibleViews.contains(entry.getKey())) { + builder.put(entry); + } } return builder.build(); } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/ViewSchemaProvider.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/ViewSchemaProvider.java new file mode 100644 index 000000000000..a332728b9158 --- /dev/null +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/ViewSchemaProvider.java @@ -0,0 +1,63 @@ +/* + * 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.druid.sql.calcite.schema; + +import com.google.common.base.Preconditions; +import com.google.inject.Inject; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.sql.calcite.planner.PlannerConfig; +import org.apache.druid.sql.calcite.view.ViewManager; + +import java.util.List; + +public class ViewSchemaProvider implements SchemaProvider +{ + private final ViewManager viewManager; + private final AuthorizerMapper authorizerMapper; + private final PlannerConfig plannerConfig; + + @Inject + public ViewSchemaProvider( + final ViewManager viewManager, + final AuthorizerMapper authorizerMapper, + final PlannerConfig plannerConfig + ) + { + this.viewManager = Preconditions.checkNotNull(viewManager, "viewManager"); + this.authorizerMapper = Preconditions.checkNotNull(authorizerMapper, "authorizerMapper"); + this.plannerConfig = Preconditions.checkNotNull(plannerConfig, "plannerConfig"); + } + + @Override + public List getSchemas(AuthenticationResult authenticationResult) + { + return List.of( + new NamedViewSchema( + new ViewSchema( + viewManager, + authorizerMapper, + authenticationResult, + plannerConfig.isAuthorizeTableVisibility() + ) + ) + ); + } +} diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/view/DruidViewMacro.java b/sql/src/main/java/org/apache/druid/sql/calcite/view/DruidViewMacro.java index 5c967bb0aeb4..94dc3bde72c2 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/view/DruidViewMacro.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/view/DruidViewMacro.java @@ -28,6 +28,7 @@ import org.apache.calcite.schema.TableMacro; import org.apache.calcite.schema.TranslatableTable; import org.apache.calcite.schema.impl.ViewTable; +import org.apache.druid.server.security.Escalator; import org.apache.druid.sql.calcite.parser.DruidSqlParser; import org.apache.druid.sql.calcite.planner.DruidPlanner; import org.apache.druid.sql.calcite.planner.PlannerFactory; @@ -41,28 +42,34 @@ public class DruidViewMacro implements TableMacro private final PlannerFactory plannerFactory; private final String viewSql; private final String druidSchemaName; + private final Escalator escalator; @Inject public DruidViewMacro( @Assisted final PlannerFactory plannerFactory, @Assisted final String viewSql, - @DruidSchemaName String druidSchemaName + @DruidSchemaName final String druidSchemaName, + final Escalator escalator ) { this.plannerFactory = plannerFactory; this.viewSql = viewSql; this.druidSchemaName = druidSchemaName; + this.escalator = escalator; } @Override public TranslatableTable apply(final List arguments) { final RelDataType rowType; + + // Determine the row type of the view using an escalated planner, which will be able to see all tables. try (final DruidPlanner planner = plannerFactory.createPlanner( ViewSqlEngine.INSTANCE, viewSql, DruidSqlParser.parse(viewSql, false).getMainStatement(), // views cannot embed SET + escalator.createEscalatedAuthenticationResult(), Collections.emptySet(), Collections.emptyMap(), null @@ -71,9 +78,6 @@ public TranslatableTable apply(final List arguments) planner.validate(); rowType = planner.prepare().getValidatedRowType(); } - catch (Exception e) { - throw new RuntimeException(e); - } return new ViewTable( null, diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/view/ViewManager.java b/sql/src/main/java/org/apache/druid/sql/calcite/view/ViewManager.java index b48280f1a61b..523f054c9f51 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/view/ViewManager.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/view/ViewManager.java @@ -19,13 +19,28 @@ package org.apache.druid.sql.calcite.view; +import org.apache.druid.query.policy.Policy; +import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.planner.PlannerFactory; +import org.apache.druid.sql.calcite.schema.ViewSchema; import java.util.Map; /** - * View managers allow {@link org.apache.druid.sql.calcite.schema.DruidSchema} to support views. They must be - * thread-safe. + * View managers appear in the {@link ViewSchema}. They are not currently exposed via user-facing API, but may + * be exposed in the future. View managers must be thread-safe. + * + *

Access to views is authorized using {@link ResourceType#VIEW}. Views are expanded by {@link DruidViewMacro} + * using escalated privileges, not the privileges of the user running the query. This means that views are a + * security boundary: it is possible for a user to have access to a view {@code aview} that references + * a table {@code atable} that the user does *not* have access to. + * + *

Views are treated as owned by the superuser (superuser privileges are used for view expansion). Therefore, + * users must not be allowed to create their own views, as this would enable them to access tables that they may + * not otherwise have had access to. + * + *

Note that for tables reached entirely through views, policies ({@link Policy}) are not attached. This is + * consistent with the idea that views are expanded as the superuser. */ public interface ViewManager { diff --git a/sql/src/main/java/org/apache/druid/sql/guice/SqlBindings.java b/sql/src/main/java/org/apache/druid/sql/guice/SqlBindings.java index 4dcf5165fa01..cdb87c2d9b12 100644 --- a/sql/src/main/java/org/apache/druid/sql/guice/SqlBindings.java +++ b/sql/src/main/java/org/apache/druid/sql/guice/SqlBindings.java @@ -23,10 +23,12 @@ import com.google.inject.Key; import com.google.inject.Scopes; import com.google.inject.multibindings.Multibinder; +import org.apache.druid.guice.LazySingleton; import org.apache.druid.guice.PolyBind; import org.apache.druid.sql.calcite.aggregation.SqlAggregator; import org.apache.druid.sql.calcite.expression.SqlOperatorConversion; import org.apache.druid.sql.calcite.schema.NamedSchema; +import org.apache.druid.sql.calcite.schema.SchemaProvider; /** * Utility class that provides bindings to extendable components in the SqlModule @@ -68,7 +70,7 @@ public static void addOperatorConversion( } /** - * Returns a multiBinder that can modules can use to bind {@link NamedSchema} to be used by the SqlModule + * Binds a {@link NamedSchema} available to all users. */ public static void addSchema( final Binder binder, @@ -78,4 +80,16 @@ public static void addSchema( binder.bind(clazz).in(Scopes.SINGLETON); Multibinder.newSetBinder(binder, NamedSchema.class).addBinding().to(clazz); } + + /** + * Binds a {@link SchemaProvider} that provides user-specific schemas. + * All providers are bound as {@link LazySingleton}. + */ + public static void addSchemaProvider( + final Binder binder, + final Class clazz + ) + { + Multibinder.newSetBinder(binder, SchemaProvider.class).addBinding().to(clazz).in(LazySingleton.class); + } } diff --git a/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java b/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java index 3880e4c2a51c..4abb504c5243 100644 --- a/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java +++ b/sql/src/test/java/org/apache/druid/sql/SqlStatementTest.java @@ -61,7 +61,7 @@ import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.planner.PlannerFactory; import org.apache.druid.sql.calcite.planner.PrepareResult; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.hook.DruidHookDispatcher; import org.easymock.EasyMock; @@ -538,8 +538,9 @@ public void testIgnoredQueryContextParametersAreIgnored() private SqlStatementFactory buildSqlStatementFactory() { - final PlannerConfig plannerConfig = PlannerConfig.builder().build(); - final DruidSchemaCatalog rootSchema = CalciteTests.createMockRootSchema( + // Set authorizeTableVisibility(false) so reads from unauthorized tables are "Forbidden" rather than "Not Found". + final PlannerConfig plannerConfig = PlannerConfig.builder().authorizeTableVisibility(false).build(); + final DruidSchemaCatalogProvider rootSchemaProvider = CalciteTests.createMockRootSchemaProvider( conglomerate, walker, plannerConfig, @@ -552,7 +553,7 @@ private SqlStatementFactory buildSqlStatementFactory() final JoinableFactoryWrapper joinableFactoryWrapper = CalciteTests.createJoinableFactoryWrapper(); final PlannerFactory plannerFactory = new PlannerFactory( - rootSchema, + rootSchemaProvider, operatorTable, macroTable, plannerConfig, diff --git a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java index 1c0a68aa86a7..9ea180c6f469 100644 --- a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java +++ b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java @@ -31,7 +31,6 @@ import com.google.common.util.concurrent.MoreExecutors; import com.google.inject.Injector; import com.google.inject.TypeLiteral; -import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; import org.apache.calcite.avatica.AvaticaClientRuntimeException; import org.apache.calcite.avatica.AvaticaSqlException; @@ -72,7 +71,6 @@ import org.apache.druid.server.log.RequestLogger; import org.apache.druid.server.log.TestRequestLogger; import org.apache.druid.server.metrics.NoopServiceEmitter; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthTestUtils; import org.apache.druid.server.security.AuthenticatorMapper; @@ -87,9 +85,8 @@ import org.apache.druid.sql.calcite.planner.DruidOperatorTable; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.planner.PlannerFactory; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchemaName; -import org.apache.druid.sql.calcite.schema.NamedSchema; import org.apache.druid.sql.calcite.util.CalciteTestBase; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.calcite.util.QueryFrameworkUtils; @@ -97,8 +94,11 @@ import org.apache.druid.sql.guice.SqlModule; import org.apache.druid.sql.hook.DruidHookDispatcher; import org.eclipse.jetty.server.Server; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; +import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -196,9 +196,9 @@ public static void tearDownClass() throws IOException private Injector injector; private TestRequestLogger testRequestLogger; - private DruidSchemaCatalog makeRootSchema() + private DruidSchemaCatalogProvider makeRootSchemaProvider() { - return CalciteTests.createMockRootSchema( + return CalciteTests.createMockRootSchemaProvider( conglomerate, walker, plannerConfig, @@ -274,7 +274,7 @@ protected DruidAvaticaHandler getAvaticaHandler(final DruidMeta druidMeta) @BeforeEach public void setUp() throws Exception { - final DruidSchemaCatalog rootSchema = makeRootSchema(); + final DruidSchemaCatalogProvider rootSchemaProvider = makeRootSchemaProvider(); testRequestLogger = new TestRequestLogger(); injector = new CoreInjectorBuilder(new StartupInjectorBuilder().build()) @@ -294,10 +294,7 @@ public void setUp() throws Exception .toInstance(new DefaultQueryConfig(ImmutableMap.of("forbidden-key", "system-default-value"))); binder.bind(QueryConfigProvider.class).to(DefaultQueryConfig.class); binder.bind(RequestLogger.class).toInstance(testRequestLogger); - binder.bind(DruidSchemaCatalog.class).toInstance(rootSchema); - for (NamedSchema schema : rootSchema.getNamedSchemas().values()) { - Multibinder.newSetBinder(binder, NamedSchema.class).addBinding().toInstance(schema); - } + binder.bind(DruidSchemaCatalogProvider.class).toInstance(rootSchemaProvider); binder.bind(QueryLifecycleFactory.class) .toInstance(CalciteTests.createMockQueryLifecycleFactory(walker, conglomerate)); binder.bind(DruidOperatorTable.class).toInstance(operatorTable); @@ -599,6 +596,12 @@ public void testDatabaseMetaDataTables() throws SQLException Pair.of("TABLE_SCHEM", "druid"), Pair.of("TABLE_TYPE", "TABLE") ), + row( + Pair.of("TABLE_CAT", "druid"), + Pair.of("TABLE_NAME", CalciteTests.READ_ONLY_DATASOURCE), + Pair.of("TABLE_SCHEM", "druid"), + Pair.of("TABLE_TYPE", "TABLE") + ), row( Pair.of("TABLE_CAT", "druid"), Pair.of("TABLE_NAME", CalciteTests.RESTRICTED_BROADCAST_DATASOURCE), @@ -709,6 +712,12 @@ public void testDatabaseMetaDataTablesAsSuperuser() throws SQLException Pair.of("TABLE_SCHEM", "druid"), Pair.of("TABLE_TYPE", "TABLE") ), + row( + Pair.of("TABLE_CAT", "druid"), + Pair.of("TABLE_NAME", CalciteTests.READ_ONLY_DATASOURCE), + Pair.of("TABLE_SCHEM", "druid"), + Pair.of("TABLE_TYPE", "TABLE") + ), row( Pair.of("TABLE_CAT", "druid"), Pair.of("TABLE_NAME", CalciteTests.RESTRICTED_BROADCAST_DATASOURCE), @@ -1121,7 +1130,7 @@ private SqlStatementFactory makeStatementFactory() return QueryFrameworkUtils.createSqlStatementFactory( CalciteTests.createMockSqlEngine(walker, conglomerate), new PlannerFactory( - makeRootSchema(), + makeRootSchemaProvider(), operatorTable, macroTable, plannerConfig, @@ -1706,48 +1715,26 @@ public void testArrayStuff() throws SQLException } /** - * Verify that a security exception is mapped to the correct Avatica SQL error codes. + * Verify that a table the user cannot read is not visible at all. */ @Test public void testUnauthorizedTable() { final String query = "SELECT * FROM " + CalciteTests.FORBIDDEN_DATASOURCE; - final String expectedError = "Error 2 (00002) : Error while executing SQL \"" + - query + "\": Remote driver error: " + Access.DEFAULT_ERROR_MESSAGE; - try (Statement statement = client.createStatement()) { - statement.executeQuery(query); - } - catch (SQLException e) { - Assertions.assertEquals( - e.getMessage(), - expectedError - ); - return; - } - Assertions.fail("Test failed, did not get SQLException"); - } - - private static class TestResultFetcher extends ResultFetcher - { - public TestResultFetcher(int limit, Yielder yielder) - { - super(limit, yielder); - } - - @Override - public Meta.Frame call() - { - try { - if (offset() == 0) { - System.out.println("Taking a nap now..."); - Thread.sleep(3000); + final SQLException e = Assertions.assertThrows( + SQLException.class, + () -> { + try (Statement statement = client.createStatement()) { + statement.executeQuery(query); + } } - } - catch (InterruptedException e) { - throw new RuntimeException(e); - } - return super.call(); - } + ); + MatcherAssert.assertThat( + e, + ThrowableMessageMatcher.hasMessage( + CoreMatchers.containsString("Object '" + CalciteTests.FORBIDDEN_DATASOURCE + "' not found") + ) + ); } /** @@ -1903,9 +1890,7 @@ public void testMultiPreparedStatementFails() throws SQLException // in reality, but handled at the JDBC level below DBI.) private void testWithJDBI(String baseUrl) { - String url = baseUrl + "?user=regularUser&password=druid" + getJdbcUrlTail(); - System.out.println(url); - DBI dbi = new DBI(url); + DBI dbi = new DBI(baseUrl, "regularUser", "druid"); Handle handle = dbi.open(); try { ResultIterator> iter = handle diff --git a/sql/src/test/java/org/apache/druid/sql/avatica/DruidStatementTest.java b/sql/src/test/java/org/apache/druid/sql/avatica/DruidStatementTest.java index f942c5ecc412..cd669d751052 100644 --- a/sql/src/test/java/org/apache/druid/sql/avatica/DruidStatementTest.java +++ b/sql/src/test/java/org/apache/druid/sql/avatica/DruidStatementTest.java @@ -44,7 +44,7 @@ import org.apache.druid.sql.calcite.planner.DruidOperatorTable; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.planner.PlannerFactory; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.util.CalciteTestBase; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.calcite.util.QueryFrameworkUtils; @@ -100,11 +100,15 @@ public void setUp() final PlannerConfig plannerConfig = new PlannerConfig(); final DruidOperatorTable operatorTable = CalciteTests.createOperatorTable(); final ExprMacroTable macroTable = CalciteTests.createExprMacroTable(); - DruidSchemaCatalog rootSchema = - CalciteTests.createMockRootSchema(conglomerate, walker, plannerConfig, AuthTestUtils.TEST_AUTHORIZER_MAPPER); + final DruidSchemaCatalogProvider rootSchemaProvider = CalciteTests.createMockRootSchemaProvider( + conglomerate, + walker, + plannerConfig, + AuthTestUtils.TEST_AUTHORIZER_MAPPER + ); final JoinableFactoryWrapper joinableFactoryWrapper = CalciteTests.createJoinableFactoryWrapper(); final PlannerFactory plannerFactory = new PlannerFactory( - rootSchema, + rootSchemaProvider, operatorTable, macroTable, plannerConfig, diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java index 4fc42ff2a303..dfdb3ac4227c 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java @@ -1192,16 +1192,6 @@ public void analyzeResources( .run(); } - public SqlStatementFactory getSqlStatementFactory( - PlannerConfig plannerConfig - ) - { - return getSqlStatementFactory( - plannerConfig, - new AuthConfig() - ); - } - /** * Build the statement factory, which also builds all the infrastructure * behind the factory by calling methods on this test class. As a result, each diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteCatalogIngestionDmlTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteCatalogIngestionDmlTest.java index 6aabb1f69074..4a2b99b5bb8f 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteCatalogIngestionDmlTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteCatalogIngestionDmlTest.java @@ -468,7 +468,7 @@ public void testInsertHourGrainPartitonedByFromCatalog() "SELECT * FROM foo") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("hourDs", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceWrite("hourDs"), dataSourceRead("foo")) + .expectResources(dataSourceRead("hourDs"), dataSourceWrite("hourDs"), dataSourceRead("foo")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -494,7 +494,7 @@ public void testInsertHourGrainWithDayPartitonedByFromQuery() "PARTITIONED BY day") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("hourDs", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceWrite("hourDs"), dataSourceRead("foo")) + .expectResources(dataSourceRead("hourDs"), dataSourceWrite("hourDs"), dataSourceRead("foo")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -539,7 +539,7 @@ public void testInsertNoPartitonedByWithDayPartitonedByFromQuery() "PARTITIONED BY day") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("noPartitonedBy", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceWrite("noPartitonedBy"), dataSourceRead("foo")) + .expectResources(dataSourceRead("noPartitonedBy"), dataSourceWrite("noPartitonedBy"), dataSourceRead("foo")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -593,7 +593,7 @@ public void testInsertAddNonDefinedColumnIntoNonSealedCatalogTable() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("foo", signature) - .expectResources(dataSourceWrite("foo"), Externals.externalRead("EXTERNAL")) + .expectResources(dataSourceRead("foo"), dataSourceWrite("foo"), Externals.externalRead("EXTERNAL")) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -650,7 +650,11 @@ public void testInsertTableWithClusteringWithClusteringFromCatalog() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("tableWithClustering", signature) - .expectResources(dataSourceWrite("tableWithClustering"), Externals.externalRead("EXTERNAL")) + .expectResources( + dataSourceRead("tableWithClustering"), + dataSourceWrite("tableWithClustering"), + Externals.externalRead("EXTERNAL") + ) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -711,7 +715,11 @@ public void testInsertTableWithClusteringWithClusteringFromQuery() "CLUSTERED BY dim1") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("tableWithClustering", signature) - .expectResources(dataSourceWrite("tableWithClustering"), Externals.externalRead("EXTERNAL")) + .expectResources( + dataSourceRead("tableWithClustering"), + dataSourceWrite("tableWithClustering"), + Externals.externalRead("EXTERNAL") + ) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -775,7 +783,11 @@ public void testInsertTableWithClusteringWithClusteringOnNewColumnFromQuery() "CLUSTERED BY dim3") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("tableWithClustering", signature) - .expectResources(dataSourceWrite("tableWithClustering"), Externals.externalRead("EXTERNAL")) + .expectResources( + dataSourceRead("tableWithClustering"), + dataSourceWrite("tableWithClustering"), + Externals.externalRead("EXTERNAL") + ) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -948,7 +960,7 @@ public void testGroupByInsertAddNonDefinedColumnIntoNonSealedCatalogTable() ) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("foo", signature) - .expectResources(dataSourceWrite("foo"), Externals.externalRead("EXTERNAL")) + .expectResources(dataSourceRead("foo"), dataSourceWrite("foo"), Externals.externalRead("EXTERNAL")) .expectQuery( GroupByQuery.builder() .setDataSource(externalDataSource) @@ -1060,7 +1072,7 @@ public void testInsertAddNonDefinedColumnIntoSealedCatalogTableAndValidationDisa "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("fooSealed", signature) - .expectResources(dataSourceWrite("fooSealed"), Externals.externalRead("EXTERNAL")) + .expectResources(dataSourceRead("fooSealed"), dataSourceWrite("fooSealed"), Externals.externalRead("EXTERNAL")) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -1116,7 +1128,11 @@ public void testInsertIntoBaseTableCatalogTable() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("tableWithBaseTable", signature) - .expectResources(dataSourceWrite("tableWithBaseTable"), Externals.externalRead("EXTERNAL")) + .expectResources( + dataSourceRead("tableWithBaseTable"), + dataSourceWrite("tableWithBaseTable"), + Externals.externalRead("EXTERNAL") + ) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -1234,7 +1250,7 @@ public void testInsertWithSourceIntoCatalogTable() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("foo", signature) - .expectResources(dataSourceWrite("foo"), Externals.externalRead("EXTERNAL")) + .expectResources(dataSourceRead("foo"), dataSourceWrite("foo"), Externals.externalRead("EXTERNAL")) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -1302,7 +1318,7 @@ public void testGroupByInsertWithSourceIntoCatalogTable() ) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("foo", signature) - .expectResources(dataSourceWrite("foo"), Externals.externalRead("EXTERNAL")) + .expectResources(dataSourceRead("foo"), dataSourceWrite("foo"), Externals.externalRead("EXTERNAL")) .expectQuery( GroupByQuery.builder() .setDataSource(externalDataSource) diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteExportTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteExportTest.java index 273cec8ce246..7965bd7f14b3 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteExportTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteExportTest.java @@ -296,7 +296,7 @@ public void testSelectFromTableNamedExport() .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST) .build() ) - .expectResources(dataSourceRead("foo"), dataSourceWrite("csv")) + .expectResources(dataSourceRead("foo"), dataSourceRead("csv"), dataSourceWrite("csv")) .expectTarget("csv", RowSignature.builder().add("dim2", ColumnType.STRING).build()) .verify(); } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteIngestionDmlTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteIngestionDmlTest.java index 55241090f14e..048a3c498ca2 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteIngestionDmlTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteIngestionDmlTest.java @@ -276,6 +276,12 @@ protected IngestionDmlTester sql(final String sqlPattern, final Object arg, fina return this; } + public IngestionDmlTester plannerConfig(final PlannerConfig plannerConfig) + { + this.plannerConfig = plannerConfig; + return this; + } + public IngestionDmlTester context(final Map context) { this.queryContext = context; diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java index d2bc4f8fe515..f717fa97969e 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteInsertDmlTest.java @@ -50,6 +50,7 @@ import org.apache.druid.sql.calcite.filtration.Filtration; import org.apache.druid.sql.calcite.parser.DruidSqlInsert; import org.apache.druid.sql.calcite.planner.Calcites; +import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.util.CalciteTests; import org.junit.jupiter.api.Assertions; @@ -80,7 +81,7 @@ public void testInsertFromTable() testIngestionQuery() .sql("INSERT INTO dst SELECT * FROM foo PARTITIONED BY ALL TIME") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -99,7 +100,7 @@ public void testInsertFromViewA() testIngestionQuery() .sql("INSERT INTO dst SELECT * FROM view.aview PARTITIONED BY ALL TIME") .expectTarget("dst", RowSignature.builder().add("dim1_firstchar", ColumnType.STRING).build()) - .expectResources(viewRead("aview"), dataSourceWrite("dst")) + .expectResources(viewRead("aview"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -127,7 +128,7 @@ public void testInsertFromViewC() testIngestionQuery() .sql("INSERT INTO dst SELECT * FROM view.cview PARTITIONED BY ALL TIME") .expectTarget("dst", expectedSignature) - .expectResources(viewRead("cview"), dataSourceWrite("dst")) + .expectResources(viewRead("cview"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource( @@ -194,7 +195,7 @@ public void testInsertIntoQualifiedTable() testIngestionQuery() .sql("INSERT INTO druid.dst SELECT * FROM foo PARTITIONED BY ALL TIME") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -281,7 +282,8 @@ public void testInsertFromUnauthorizedDataSource() { testIngestionQuery() .sql("INSERT INTO dst SELECT * FROM \"%s\" PARTITIONED BY ALL TIME", CalciteTests.FORBIDDEN_DATASOURCE) - .expectValidationError(ForbiddenException.class) + .expectValidationError(DruidExceptionMatcher.invalidSqlInput() + .expectMessageContains("Object 'forbiddenDatasource' not found")) .verify(); } @@ -294,6 +296,56 @@ public void testInsertIntoUnauthorizedDataSource() .verify(); } + @Test + public void testInsertFromReadOnlyDataSource() + { + testIngestionQuery() + .sql( + "INSERT INTO dst SELECT __time, dim1 FROM \"%s\" PARTITIONED BY ALL TIME", + CalciteTests.READ_ONLY_DATASOURCE + ) + .expectTarget( + "dst", + RowSignature.builder().addTimeColumn().add("dim1", ColumnType.STRING).build() + ) + .expectResources( + dataSourceRead(CalciteTests.READ_ONLY_DATASOURCE), + dataSourceRead("dst"), + dataSourceWrite("dst") + ) + .expectQuery( + newScanQueryBuilder() + .dataSource(CalciteTests.READ_ONLY_DATASOURCE) + .intervals(querySegmentSpec(Filtration.eternity())) + .columns("__time", "dim1") + .columnTypes(ColumnType.LONG, ColumnType.STRING) + .context(PARTITIONED_BY_ALL_TIME_QUERY_CONTEXT) + .build() + ) + .verify(); + } + + @Test + public void testInsertIntoReadOnlyDataSource() + { + // This INSERT is forbidden because the user cannot write to a readOnly datasource. + testIngestionQuery() + .sql("INSERT INTO \"%s\" SELECT * FROM foo PARTITIONED BY ALL TIME", CalciteTests.READ_ONLY_DATASOURCE) + .expectValidationError(ForbiddenException.class) + .verify(); + } + + @Test + public void testInsertIntoReadOnlyDataSource_noAuthorizeTableVisibility() + { + // This INSERT is forbidden because the user cannot write to a readOnly datasource. + testIngestionQuery() + .sql("INSERT INTO \"%s\" SELECT * FROM foo PARTITIONED BY ALL TIME", CalciteTests.READ_ONLY_DATASOURCE) + .plannerConfig(PlannerConfig.builder().authorizeTableVisibility(false).build()) + .expectValidationError(ForbiddenException.class) + .verify(); + } + @Test public void testInsertIntoNonexistentSchema() { @@ -312,7 +364,7 @@ public void testInsertFromExternal() .sql("INSERT INTO dst SELECT * FROM %s PARTITIONED BY ALL TIME", externSql(externalDataSource)) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -334,7 +386,7 @@ public void testInsertFromExternalWithInputSourceSecurityEnabled() .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .authConfig(AuthConfig.newBuilder().setEnableInputSourceSecurity(true).build()) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), externalRead("inline")) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), externalRead("inline")) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -392,7 +444,7 @@ public void testInsertFromExternalWithSchema() ) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -438,7 +490,7 @@ public void testInsertFromExternalWithSchemaWithInputsourceSecurity() .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .authConfig(AuthConfig.newBuilder().setEnableInputSourceSecurity(true).build()) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), externalRead("inline")) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), externalRead("inline")) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -482,7 +534,7 @@ public void testInsertFromExternalFunctionalStyleWithSchemaWithInputsourceSecuri .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .authConfig(AuthConfig.newBuilder().setEnableInputSourceSecurity(true).build()) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), externalRead("inline")) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), externalRead("inline")) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -535,7 +587,7 @@ public void testInsertFromExternalWithoutSecuritySupport() .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .authConfig(AuthConfig.newBuilder().setEnableInputSourceSecurity(false).build()) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -596,7 +648,7 @@ public void testInsertWithPartitionedBy() .sql( "INSERT INTO druid.dst SELECT __time, FLOOR(m1) as floor_m1, dim1 FROM foo PARTITIONED BY TIME_FLOOR(__time, 'PT1H')") .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -650,7 +702,7 @@ public void testPartitionedBySupportedClauses() partitionedByArgument )) .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -697,7 +749,7 @@ public void testPartitionedBySupportedGranularityLiteralClauses() partitionedByArgument )) .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -971,7 +1023,7 @@ public void testInsertWithClusteredBy() + "PARTITIONED BY FLOOR(__time TO DAY) CLUSTERED BY 2, dim1, CEIL(m2)" ) .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -1013,7 +1065,7 @@ public void testInsertPeriodFormGranularityWithClusteredBy() + "PARTITIONED BY P1D CLUSTERED BY 2, dim1, CEIL(m2)" ) .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -1067,7 +1119,7 @@ public void testInsertWithPartitionedByAndClusteredBy() .sql( "INSERT INTO druid.dst SELECT __time, FLOOR(m1) as floor_m1, dim1 FROM foo PARTITIONED BY DAY CLUSTERED BY 2, dim1") .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -1100,7 +1152,7 @@ public void testInsertWithPartitionedByAndLimitOffset() .sql( "INSERT INTO druid.dst SELECT __time, FLOOR(m1) as floor_m1, dim1 FROM foo LIMIT 10 OFFSET 20 PARTITIONED BY DAY") .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -1373,7 +1425,7 @@ public void testInsertFromExternalProjectSort() ) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", RowSignature.builder().add("xy", ColumnType.STRING).add("z", ColumnType.LONG).build()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -1412,7 +1464,7 @@ public void testInsertFromExternalAggregate() .add("cnt", ColumnType.LONG) .build() ) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( GroupByQuery.builder() .setDataSource(externalDataSource) @@ -1446,7 +1498,7 @@ public void testInsertFromExternalAggregateAll() .add("cnt", ColumnType.LONG) .build() ) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( GroupByQuery.builder() .setDataSource(externalDataSource) diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java index 3c2cff27729d..294c148a9b5d 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java @@ -173,14 +173,70 @@ public void testInformationSchemaSchemata() } @Test - public void testInformationSchemaTables() + public void testInformationSchemaTables_regularUser() { msqIncompatible(); testQuery( - "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, IS_JOINABLE, IS_BROADCAST\n" - + "FROM INFORMATION_SCHEMA.TABLES\n" - + "WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW')\n" - + "ORDER BY TABLE_SCHEMA, TABLE_NAME", + PLANNER_CONFIG_DEFAULT, + """ + SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, IS_JOINABLE, IS_BROADCAST + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW') + ORDER BY TABLE_SCHEMA, TABLE_NAME""", + CalciteTests.REGULAR_USER_AUTH_RESULT, + ImmutableList.of(), + ImmutableList.builder() + .add(new Object[]{"INFORMATION_SCHEMA", "COLUMNS", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"INFORMATION_SCHEMA", "ROUTINES", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"INFORMATION_SCHEMA", "SCHEMATA", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"INFORMATION_SCHEMA", "TABLES", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.ARRAYS_DATASOURCE, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.BROADCAST_DATASOURCE, "TABLE", "YES", "YES"}) + .add(new Object[]{"druid", CalciteTests.DATASOURCE1, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.DATASOURCE2, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.DATASOURCE4, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", TestDataSet.LARRY.getName(), "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.DATASOURCE5, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.DATASOURCE3, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.READ_ONLY_DATASOURCE, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.RESTRICTED_BROADCAST_DATASOURCE, "TABLE", "YES", "YES"}) + .add(new Object[]{"druid", CalciteTests.RESTRICTED_DATASOURCE, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.SOME_DATASOURCE, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.SOMEXDATASOURCE, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.USERVISITDATASOURCE, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.WIKIPEDIA, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.WIKIPEDIA_FIRST_LAST, "TABLE", "NO", "NO"}) + .add(new Object[]{"lookup", "lookyloo", "TABLE", "YES", "YES"}) + .add(new Object[]{"lookup", "lookyloo-chain", "TABLE", "YES", "YES"}) + .add(new Object[]{"lookup", "lookyloo121", "TABLE", "YES", "YES"}) + .add(new Object[]{"sys", "segments", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"sys", "server_properties", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"sys", "server_segments", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"sys", "servers", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"sys", "supervisors", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"sys", "tasks", "SYSTEM_TABLE", "NO", "NO"}) + .add(new Object[]{"view", "aview", "VIEW", "NO", "NO"}) + .add(new Object[]{"view", "bview", "VIEW", "NO", "NO"}) + .add(new Object[]{"view", "cview", "VIEW", "NO", "NO"}) + .add(new Object[]{"view", "dview", "VIEW", "NO", "NO"}) + .add(new Object[]{"view", "invalidView", "VIEW", "NO", "NO"}) + .add(new Object[]{"view", "restrictedView", "VIEW", "NO", "NO"}) + .build() + ); + } + + @Test + public void testInformationSchemaTables_regularUser_noAuthorizeTableVisibility() + { + msqIncompatible(); + testQuery( + PlannerConfig.builder().authorizeTableVisibility(false).build(), + """ + SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, IS_JOINABLE, IS_BROADCAST + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW') + ORDER BY TABLE_SCHEMA, TABLE_NAME""", + CalciteTests.REGULAR_USER_AUTH_RESULT, ImmutableList.of(), ImmutableList.builder() .add(new Object[]{"INFORMATION_SCHEMA", "COLUMNS", "SYSTEM_TABLE", "NO", "NO"}) @@ -195,6 +251,7 @@ public void testInformationSchemaTables() .add(new Object[]{"druid", TestDataSet.LARRY.getName(), "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE5, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE3, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.READ_ONLY_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.RESTRICTED_BROADCAST_DATASOURCE, "TABLE", "YES", "YES"}) .add(new Object[]{"druid", CalciteTests.RESTRICTED_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.SOME_DATASOURCE, "TABLE", "NO", "NO"}) @@ -219,13 +276,18 @@ public void testInformationSchemaTables() .add(new Object[]{"view", "restrictedView", "VIEW", "NO", "NO"}) .build() ); + } + @Test + public void testInformationSchemaTables_superUser() + { testQuery( PLANNER_CONFIG_DEFAULT, - "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, IS_JOINABLE, IS_BROADCAST\n" - + "FROM INFORMATION_SCHEMA.TABLES\n" - + "WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW')\n" - + "ORDER BY TABLE_SCHEMA, TABLE_NAME", + """ + SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE, IS_JOINABLE, IS_BROADCAST + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW') + ORDER BY TABLE_SCHEMA, TABLE_NAME""", CalciteTests.SUPER_USER_AUTH_RESULT, ImmutableList.of(), ImmutableList.builder() @@ -242,6 +304,7 @@ public void testInformationSchemaTables() .add(new Object[]{"druid", TestDataSet.LARRY.getName(), "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE5, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE3, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.READ_ONLY_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.RESTRICTED_BROADCAST_DATASOURCE, "TABLE", "YES", "YES"}) .add(new Object[]{"druid", CalciteTests.RESTRICTED_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.SOME_DATASOURCE, "TABLE", "NO", "NO"}) @@ -274,9 +337,10 @@ public void testInformationSchemaColumnsOnTable() { msqIncompatible(); testQuery( - "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE\n" - + "FROM INFORMATION_SCHEMA.COLUMNS\n" - + "WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'foo'", + """ + SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'foo'""", ImmutableList.of(), ImmutableList.of( new Object[]{"__time", "TIMESTAMP", "NO"}, @@ -292,22 +356,46 @@ public void testInformationSchemaColumnsOnTable() } @Test - public void testInformationSchemaColumnsOnForbiddenTable() + public void testInformationSchemaColumnsOnForbiddenTable_regularUser() { msqIncompatible(); testQuery( - "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE\n" - + "FROM INFORMATION_SCHEMA.COLUMNS\n" - + "WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'forbiddenDatasource'", + PLANNER_CONFIG_DEFAULT, + """ + SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'forbiddenDatasource'""", + CalciteTests.REGULAR_USER_AUTH_RESULT, ImmutableList.of(), ImmutableList.of() ); + } + @Test + public void testInformationSchemaColumnsOnForbiddenTable_regularUser_noAuthorizeTableVisibility() + { + msqIncompatible(); + testQuery( + PlannerConfig.builder().authorizeTableVisibility(false).build(), + """ + SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'forbiddenDatasource'""", + CalciteTests.REGULAR_USER_AUTH_RESULT, + ImmutableList.of(), + ImmutableList.of() + ); + } + + @Test + public void testInformationSchemaColumnsOnForbiddenTable_superUser() + { testQuery( PLANNER_CONFIG_DEFAULT, - "SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE\n" - + "FROM INFORMATION_SCHEMA.COLUMNS\n" - + "WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'forbiddenDatasource'", + """ + SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = 'druid' AND TABLE_NAME = 'forbiddenDatasource'""", CalciteTests.SUPER_USER_AUTH_RESULT, ImmutableList.of(), ImmutableList.of( @@ -4145,6 +4233,15 @@ public void testCountNullableExpression() ); } + @Test + public void testTableNameIsCaseSensitive() + { + testQueryThrows( + "SELECT COUNT(*) FROM druid.Foo", + invalidSqlContains("'Foo' not found") + ); + } + @Test public void testCountStar() { diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java index fb4e3581dc16..a429dc4d4bcc 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java @@ -47,6 +47,8 @@ import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.util.CalciteTests; import org.junit.jupiter.api.Assertions; +import org.hamcrest.CoreMatchers; +import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -85,7 +87,7 @@ public void testReplaceFromTableWithReplaceAll() testIngestionQuery() .sql("REPLACE INTO dst OVERWRITE ALL SELECT * FROM foo PARTITIONED BY ALL TIME") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -105,7 +107,7 @@ public void testReplaceFromTableWithDeleteWhereClause() .sql("REPLACE INTO dst OVERWRITE WHERE __time >= TIMESTAMP '2000-01-01 00:00:00' AND __time < TIMESTAMP '2000-01-02 00:00:00' " + "SELECT * FROM foo PARTITIONED BY DAY") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -133,7 +135,7 @@ public void testReplaceFromTableWithTimeZoneInQueryContext() .sql("REPLACE INTO dst OVERWRITE WHERE __time >= TIMESTAMP '2000-01-01 05:30:00' AND __time < TIMESTAMP '2000-01-02 05:30:00' " + "SELECT * FROM foo PARTITIONED BY DAY") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -159,7 +161,7 @@ public void testReplaceFromTableWithIntervalLargerThanOneGranularity() + "__time >= TIMESTAMP '2000-01-01' AND __time < TIMESTAMP '2000-05-01' " + "SELECT * FROM foo PARTITIONED BY MONTH") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -186,7 +188,7 @@ public void testReplaceFromTableWithComplexDeleteWhereClause() + "OR __time >= TIMESTAMP '2000-03-01' AND __time < TIMESTAMP '2000-04-01' " + "SELECT * FROM foo PARTITIONED BY MONTH") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -212,7 +214,7 @@ public void testReplaceFromTableWithBetweenClause() + "__time BETWEEN TIMESTAMP '2000-01-01' AND TIMESTAMP '2000-01-31 23:59:59.999' " + "SELECT * FROM foo PARTITIONED BY MONTH") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -339,7 +341,7 @@ public void testReplaceFromView() testIngestionQuery() .sql("REPLACE INTO dst OVERWRITE ALL SELECT * FROM view.aview PARTITIONED BY ALL TIME") .expectTarget("dst", RowSignature.builder().add("dim1_firstchar", ColumnType.STRING).build()) - .expectResources(viewRead("aview"), dataSourceWrite("dst")) + .expectResources(viewRead("aview"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -360,7 +362,7 @@ public void testReplaceIntoQualifiedTable() testIngestionQuery() .sql("REPLACE INTO druid.dst OVERWRITE ALL SELECT * FROM foo PARTITIONED BY ALL TIME") .expectTarget("dst", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -384,7 +386,7 @@ public void testReplaceContainingWithList() .add("dim3", ColumnType.STRING) .build() ) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -495,7 +497,8 @@ public void testReplaceFromUnauthorizedDataSource() { testIngestionQuery() .sql("REPLACE INTO dst OVERWRITE ALL SELECT * FROM \"%s\" PARTITIONED BY ALL TIME", CalciteTests.FORBIDDEN_DATASOURCE) - .expectValidationError(ForbiddenException.class) + .expectValidationError(DruidExceptionMatcher.invalidSqlInput() + .expectMessageContains("Object 'forbiddenDatasource' not found")) .verify(); } @@ -526,7 +529,7 @@ public void testReplaceFromExternal() .sql("REPLACE INTO dst OVERWRITE ALL SELECT * FROM %s PARTITIONED BY ALL TIME", externSql(externalDataSource)) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -552,7 +555,7 @@ public void testReplaceWithPartitionedByAndLimitOffset() .sql( "REPLACE INTO druid.dst OVERWRITE ALL SELECT __time, FLOOR(m1) as floor_m1, dim1 FROM foo LIMIT 10 OFFSET 20 PARTITIONED BY DAY") .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -587,7 +590,7 @@ public void testReplaceWithClusteredBy() .sql( "REPLACE INTO druid.dst OVERWRITE ALL SELECT __time, FLOOR(m1) as floor_m1, dim1 FROM foo PARTITIONED BY DAY CLUSTERED BY 2, dim1") .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -642,7 +645,7 @@ public void testPartitionedBySupportedGranularityLiteralClauses() partitionedByArgument )) .expectTarget("dst", targetRowSignature) - .expectResources(dataSourceRead("foo"), dataSourceWrite("dst")) + .expectResources(dataSourceRead("foo"), dataSourceRead("dst"), dataSourceWrite("dst")) .expectQuery( newScanQueryBuilder() .dataSource("foo") @@ -1021,7 +1024,7 @@ public void testReplaceFromExternalProjectSort() ) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", RowSignature.builder().add("xy", ColumnType.STRING).add("z", ColumnType.LONG).build()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -1052,7 +1055,7 @@ public void testReplaceFromExternalAggregate() .add("cnt", ColumnType.LONG) .build() ) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( GroupByQuery.builder() .setDataSource(externalDataSource) @@ -1084,7 +1087,7 @@ public void testReplaceFromExternalAggregateAll() .add("cnt", ColumnType.LONG) .build() ) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( GroupByQuery.builder() .setDataSource(externalDataSource) diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteSelectQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteSelectQueryTest.java index 59fef5a20a7a..aec8b4365c5d 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteSelectQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteSelectQueryTest.java @@ -49,13 +49,16 @@ import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.column.RowSignature; import org.apache.druid.segment.virtual.ExpressionVirtualColumn; +import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.sql.calcite.filtration.Filtration; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.util.CacheTestHelperModule.ResultCacheMode; import org.apache.druid.sql.calcite.util.CalciteTests; +import org.hamcrest.CoreMatchers; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; +import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.jupiter.api.Test; import java.util.Arrays; @@ -63,6 +66,9 @@ import java.util.List; import java.util.Map; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; + public class CalciteSelectQueryTest extends BaseCalciteQueryTest { @Test @@ -1230,69 +1236,125 @@ public void testCountRestrictedTable_shouldFilterOnPolicy() } @Test - public void testSelectStarOnForbiddenTable() + public void testSelectStarOnForbiddenTable_regularUser() { - assertQueryIsForbidden( - "SELECT * FROM druid.forbiddenDatasource", - CalciteTests.REGULAR_USER_AUTH_RESULT + final String sql = "SELECT * FROM druid.forbiddenDatasource"; + + // The regular user does not have access to forbiddenDatasource, so they shouldn't see it. + DruidException e = assertThrows( + DruidException.class, + () -> testBuilder() + .sql(sql) + .authResult(CalciteTests.REGULAR_USER_AUTH_RESULT) + .build() + .run() + ); + assertThat( + e, + ThrowableMessageMatcher.hasMessage( + CoreMatchers.containsString("Object 'forbiddenDatasource' not found within 'druid'")) ); + } - testQuery( - PLANNER_CONFIG_DEFAULT, - "SELECT * FROM druid.forbiddenDatasource", - CalciteTests.SUPER_USER_AUTH_RESULT, - ImmutableList.of( - newScanQueryBuilder() - .dataSource(CalciteTests.FORBIDDEN_DATASOURCE) - .intervals(querySegmentSpec(Filtration.eternity())) - .columns("__time", "dim1", "dim2", "cnt", "m1", "m2", "unique_dim1") - .columnTypes( - ColumnType.LONG, - ColumnType.STRING, - ColumnType.STRING, - ColumnType.LONG, - ColumnType.FLOAT, - ColumnType.DOUBLE, - ColumnType.ofComplex("hyperUnique") - ) - .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST) - .context(QUERY_CONTEXT_DEFAULT) - .build() - ), - ImmutableList.of( - new Object[]{ - timestamp("2000-01-01"), - "forbidden", - "abcd", - 1L, - 9999.0f, - null, - "\"AQAAAQAAAALFBA==\"" - }, - new Object[]{ - timestamp("2000-01-02"), - "forbidden", - "a", - 1L, - 1234.0f, - null, - "\"AQAAAQAAAALFBA==\"" - } - ) + @Test + public void testSelectStarOnForbiddenTable_regularUser_noAuthorizeTableVisibility() + { + final String sql = "SELECT * FROM druid.forbiddenDatasource"; + + // The regular user does not have access to forbiddenDatasource. When authorizeTableVisibility = false, the + // validator is aware of it, but querying is still forbidden. + ForbiddenException e = assertThrows( + ForbiddenException.class, + () -> testBuilder() + .sql(sql) + .plannerConfig(PlannerConfig.builder().authorizeTableVisibility(false).build()) + .authResult(CalciteTests.REGULAR_USER_AUTH_RESULT) + .build() + .run() ); + assertThat( + e, + ThrowableMessageMatcher.hasMessage(CoreMatchers.containsString("Unauthorized")) + ); + } + + @Test + public void testSelectStarOnForbiddenTable_superUser() + { + // The superuser can see and query forbiddenDatasource. + testBuilder() + .sql("SELECT * FROM druid.forbiddenDatasource") + .plannerConfig(PLANNER_CONFIG_DEFAULT) + .authResult(CalciteTests.SUPER_USER_AUTH_RESULT) + .expectedQueries( + ImmutableList.of( + newScanQueryBuilder() + .dataSource(CalciteTests.FORBIDDEN_DATASOURCE) + .intervals(querySegmentSpec(Filtration.eternity())) + .columns("__time", "dim1", "dim2", "cnt", "m1", "m2", "unique_dim1") + .columnTypes( + ColumnType.LONG, + ColumnType.STRING, + ColumnType.STRING, + ColumnType.LONG, + ColumnType.FLOAT, + ColumnType.DOUBLE, + ColumnType.ofComplex("hyperUnique") + ) + .resultFormat(ScanQuery.ResultFormat.RESULT_FORMAT_COMPACTED_LIST) + .context(QUERY_CONTEXT_DEFAULT) + .build() + ) + ) + .expectedResults( + ImmutableList.of( + new Object[]{ + timestamp("2000-01-01"), + "forbidden", + "abcd", + 1L, + 9999.0f, + null, + "\"AQAAAQAAAALFBA==\"" + }, + new Object[]{ + timestamp("2000-01-02"), + "forbidden", + "a", + 1L, + 1234.0f, + null, + "\"AQAAAQAAAALFBA==\"" + } + ) + ) + .run(); } @Test public void testSelectStarOnForbiddenView() { - assertQueryIsForbidden( - "SELECT * FROM view.forbiddenView", - CalciteTests.REGULAR_USER_AUTH_RESULT + final String sql = "SELECT * FROM view.forbiddenView"; + + // The regular user does not have access to forbiddenDatasource, so they shouldn't see it. + DruidException e = assertThrows( + DruidException.class, + () -> testBuilder() + .sql(sql) + .authResult(CalciteTests.REGULAR_USER_AUTH_RESULT) + .build() + .run() + ); + assertThat( + e, + ThrowableMessageMatcher.hasMessage( + CoreMatchers.containsString("Object 'forbiddenView' not found within 'view'")) ); + // The superuser can see forbiddenDatasource. testQuery( PLANNER_CONFIG_DEFAULT, - "SELECT * FROM view.forbiddenView", + sql, CalciteTests.SUPER_USER_AUTH_RESULT, ImmutableList.of( newScanQueryBuilder() diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteStrictInsertTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteStrictInsertTest.java index 8b088f5dd599..83714d2efd2f 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteStrictInsertTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteStrictInsertTest.java @@ -70,7 +70,7 @@ public void testInsertIntoExisting() testIngestionQuery() .sql("INSERT INTO druid.numfoo SELECT * FROM foo PARTITIONED BY ALL TIME") .expectTarget("numfoo", FOO_TABLE_SIGNATURE) - .expectResources(dataSourceRead("foo"), dataSourceWrite("numfoo")) + .expectResources(dataSourceRead("foo"), dataSourceRead("numfoo"), dataSourceWrite("numfoo")) .expectQuery( newScanQueryBuilder() .dataSource("foo") diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/DruidPlannerResourceAnalyzeTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/DruidPlannerResourceAnalyzeTest.java index 8c8b8152858f..5f00c9745c35 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/DruidPlannerResourceAnalyzeTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/DruidPlannerResourceAnalyzeTest.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import org.apache.druid.error.DruidException; import org.apache.druid.server.security.Action; import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.Resource; @@ -28,6 +29,9 @@ import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.util.CalciteTests; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -35,6 +39,8 @@ import java.util.List; import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertThrows; + public class DruidPlannerResourceAnalyzeTest extends BaseCalciteQueryTest { @Test @@ -336,4 +342,81 @@ public void testTableAppend() ) ); } + + @Test + public void testTableAppendInSubquery() + { + final String sql = "SELECT COUNT(*) FROM (SELECT dim1 FROM TABLE(APPEND('foo', 'numfoo'))) WHERE dim1 <> 'z'"; + + analyzeResources( + sql, + ImmutableList.of( + new ResourceAction(new Resource("foo", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("numfoo", ResourceType.DATASOURCE), Action.READ) + ) + ); + } + + @Test + public void testTableAppendJoinedWithTableAndView() + { + // Resources from the AuthorizableOperator path (APPEND) and the SqlIdentifier path (foo2, aview) must merge. + final String sql = "SELECT COUNT(*) FROM TABLE(APPEND('foo', 'numfoo')) t\n" + + "INNER JOIN foo2 ON t.dim2 = foo2.dim2\n" + + "INNER JOIN view.aview v ON t.dim1 = v.dim1_firstchar"; + + analyzeResources( + sql, + ImmutableList.of( + new ResourceAction(new Resource("foo", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("numfoo", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("foo2", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("aview", ResourceType.VIEW), Action.READ) + ) + ); + } + + @Test + public void testTableAppendRepeatedTable() + { + final String sql = "SELECT * FROM TABLE(APPEND('foo', 'foo'))"; + + analyzeResources( + sql, + ImmutableList.of( + new ResourceAction(new Resource("foo", ResourceType.DATASOURCE), Action.READ) + ) + ); + } + + @Test + public void testTableAppendUnauthorizedTable() + { + // APPEND names tables with string literals rather than identifiers, but still resolves them through the + // caller's catalog reader, so an unauthorized table is not visible. + final DruidException e = assertThrows( + DruidException.class, + () -> testBuilder() + .sql("SELECT * FROM TABLE(APPEND('foo', 'forbiddenDatasource'))") + .authResult(CalciteTests.REGULAR_USER_AUTH_RESULT) + .build() + .run() + ); + + MatcherAssert.assertThat( + e, + ThrowableMessageMatcher.hasMessage(CoreMatchers.containsString("Table [forbiddenDatasource] not found")) + ); + + // The superuser can see it. + analyzeResources( + PLANNER_CONFIG_DEFAULT, + "SELECT * FROM TABLE(APPEND('foo', 'forbiddenDatasource'))", + CalciteTests.SUPER_USER_AUTH_RESULT, + ImmutableList.of( + new ResourceAction(new Resource("foo", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("forbiddenDatasource", ResourceType.DATASOURCE), Action.READ) + ) + ); + } } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/IngestTableFunctionTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/IngestTableFunctionTest.java index 567534df6cda..fb78fcf2a65c 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/IngestTableFunctionTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/IngestTableFunctionTest.java @@ -127,7 +127,7 @@ public void testHttpExtern() .sql("INSERT INTO dst SELECT * FROM %s PARTITIONED BY ALL TIME", externSql(httpDataSource)) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", httpDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -157,7 +157,7 @@ public void testHttpFunction() .sql("INSERT INTO dst SELECT * FROM %s PARTITIONED BY ALL TIME", extern) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", httpDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -188,7 +188,7 @@ public void testHttpFunctionWithInputsourceSecurity() .authConfig(AuthConfig.newBuilder().setEnableInputSourceSecurity(true).build()) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", httpDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), externalRead("http")) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), externalRead("http")) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -231,7 +231,7 @@ public void testHttpExternByName() .sql("INSERT INTO dst SELECT *\nFROM %s\nPARTITIONED BY ALL TIME", externSqlByName(httpDataSource)) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", httpDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -262,7 +262,7 @@ public void testHttpFn() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", httpDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -314,7 +314,7 @@ public void testHttpFn2() "PARTITIONED BY HOUR") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("w000", expectedSig) - .expectResources(dataSourceWrite("w000"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("w000"), dataSourceWrite("w000"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -397,7 +397,7 @@ public void testHttpFnWithParameters() .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .parameters(Collections.singletonList(new SqlParameter(SqlType.ARRAY, new String[] {"http://foo.com/bar.csv"}))) .expectTarget("dst", httpDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -444,7 +444,7 @@ public void testHttpJson() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", httpDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -489,7 +489,7 @@ public void testHttpJsonLowercaseComplexTypePrefix() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", httpDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(httpDataSource) @@ -535,7 +535,7 @@ public void testInlineExtern() .sql("INSERT INTO dst SELECT * FROM %s PARTITIONED BY ALL TIME", externSql(externalDataSource)) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -604,7 +604,7 @@ public void testInlineExternWithExtend() externClauseFromSig(externalDataSource)) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -633,7 +633,7 @@ public void testInlineFn() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", externalDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(externalDataSource) @@ -657,7 +657,7 @@ public void testLocalExtern() .sql("INSERT INTO dst SELECT * FROM %s PARTITIONED BY ALL TIME", externSql(localDataSource)) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", localDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(localDataSource) @@ -686,7 +686,7 @@ public void testLocalFilesFn() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", localDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(localDataSource) @@ -715,7 +715,7 @@ public void testLocalFnOmitExtend() "PARTITIONED BY ALL TIME") .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", localDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(localDataSource) @@ -746,7 +746,7 @@ public void testLocalFnWithAlias() ) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", localDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(localDataSource) @@ -777,7 +777,7 @@ public void testLocalFnNotNull() ) .authentication(CalciteTests.SUPER_USER_AUTH_RESULT) .expectTarget("dst", localDataSource.getSignature()) - .expectResources(dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) + .expectResources(dataSourceRead("dst"), dataSourceWrite("dst"), Externals.EXTERNAL_RESOURCE_ACTION) .expectQuery( newScanQueryBuilder() .dataSource(localDataSource) diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/SqlVectorizedExpressionResultConsistencyTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/SqlVectorizedExpressionResultConsistencyTest.java index 4ebc253c7c5f..dccb61e0931d 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/SqlVectorizedExpressionResultConsistencyTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/SqlVectorizedExpressionResultConsistencyTest.java @@ -48,7 +48,7 @@ import org.apache.druid.sql.calcite.planner.PlannerFactory; import org.apache.druid.sql.calcite.planner.PlannerResult; import org.apache.druid.sql.calcite.run.SqlEngine; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.hook.DruidHookDispatcher; import org.apache.druid.testing.InitializedNullHandlingTest; @@ -141,12 +141,12 @@ public static void setupClass() CLOSER.register(WALKER); final PlannerConfig plannerConfig = new PlannerConfig(); - final DruidSchemaCatalog rootSchema = - CalciteTests.createMockRootSchema(CONGLOMERATE, WALKER, plannerConfig, AuthTestUtils.TEST_AUTHORIZER_MAPPER); + final DruidSchemaCatalogProvider rootSchemaProvider = + CalciteTests.createMockRootSchemaProvider(CONGLOMERATE, WALKER, plannerConfig, AuthTestUtils.TEST_AUTHORIZER_MAPPER); final JoinableFactoryWrapper joinableFactoryWrapper = CalciteTests.createJoinableFactoryWrapper(); ENGINE = CalciteTests.createMockSqlEngine(WALKER, CONGLOMERATE); PLANNER_FACTORY = new PlannerFactory( - rootSchema, + rootSchemaProvider, CalciteTests.createOperatorTable(), CalciteTests.createExprMacroTable(), plannerConfig, diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionTestHelper.java b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionTestHelper.java index ee7f03e87643..ecb0d29d0300 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionTestHelper.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionTestHelper.java @@ -55,6 +55,7 @@ import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.planner.PlannerToolbox; import org.apache.druid.sql.calcite.rel.VirtualColumnRegistry; +import org.apache.druid.sql.calcite.schema.ConstantDruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchema; import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; import org.apache.druid.sql.calcite.schema.NamedDruidSchema; @@ -88,11 +89,13 @@ public class ExpressionTestHelper CalciteTests.createExprMacroTable(), CalciteTests.getJsonMapper(), new PlannerConfig(), - new DruidSchemaCatalog( - EasyMock.createMock(SchemaPlus.class), - ImmutableMap.of( - "druid", new NamedDruidSchema(EasyMock.createMock(DruidSchema.class), "druid"), - NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) + new ConstantDruidSchemaCatalogProvider( + new DruidSchemaCatalog( + EasyMock.createMock(SchemaPlus.class), + ImmutableMap.of( + "druid", new NamedDruidSchema(EasyMock.createMock(DruidSchema.class), "druid"), + NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) + ) ) ), JOINABLE_FACTORY_WRAPPER, @@ -109,6 +112,7 @@ NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) "SELECT 1", // The actual query isn't important for this test null, /* Don't need SQL node */ null, /* Don't need engine */ + null, /* Don't need authentication result */ Collections.emptySet(), Collections.emptyMap(), null @@ -343,7 +347,6 @@ void testExpression( } ExprEval result = PLANNER_CONTEXT.parseExpression(expression.getExpression()) - .eval(expressionBindings); Assertions.assertEquals(expectedResult, result.value(), "Result for: " + rexNode); diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/external/ExternalTableScanRuleTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/external/ExternalTableScanRuleTest.java index 45f83af822c5..6ecdacf96a7d 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/external/ExternalTableScanRuleTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/external/ExternalTableScanRuleTest.java @@ -35,6 +35,7 @@ import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.planner.PlannerToolbox; import org.apache.druid.sql.calcite.run.NativeSqlEngine; +import org.apache.druid.sql.calcite.schema.ConstantDruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchema; import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; import org.apache.druid.sql.calcite.schema.NamedDruidSchema; @@ -62,11 +63,13 @@ public void testMatchesWhenExternalScanUnsupported() CalciteTests.createExprMacroTable(), CalciteTests.getJsonMapper(), new PlannerConfig(), - new DruidSchemaCatalog( - EasyMock.createMock(SchemaPlus.class), - ImmutableMap.of( - "druid", new NamedDruidSchema(EasyMock.createMock(DruidSchema.class), "druid"), - NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) + new ConstantDruidSchemaCatalogProvider( + new DruidSchemaCatalog( + EasyMock.createMock(SchemaPlus.class), + ImmutableMap.of( + "druid", new NamedDruidSchema(EasyMock.createMock(DruidSchema.class), "druid"), + NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) + ) ) ), CalciteTests.createJoinableFactoryWrapper(), @@ -83,6 +86,7 @@ NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) "SELECT 1", // The actual query isn't important for this test DruidSqlParser.parse("SELECT 1", false).getMainStatement(), engine, + null, Collections.emptySet(), Collections.emptyMap(), null diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/planner/CalcitePlannerModuleTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/planner/CalcitePlannerModuleTest.java index ee5a5b718f2e..d8adb57e867c 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/planner/CalcitePlannerModuleTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/planner/CalcitePlannerModuleTest.java @@ -47,7 +47,9 @@ import org.apache.druid.sql.calcite.parser.DruidSqlParser; import org.apache.druid.sql.calcite.rule.ExtensionCalciteRuleProvider; import org.apache.druid.sql.calcite.run.NativeSqlEngine; +import org.apache.druid.sql.calcite.schema.ConstantDruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchemaName; import org.apache.druid.sql.calcite.schema.NamedSchema; import org.apache.druid.sql.calcite.util.CalciteTestBase; @@ -138,7 +140,8 @@ public void onMatch(RelOptRuleCall call) binder.bind(String.class).annotatedWith(DruidSchemaName.class).toInstance(DRUID_SCHEMA_NAME); binder.bind(Key.get(new TypeLiteral>() {})).toInstance(aggregators); binder.bind(Key.get(new TypeLiteral>() {})).toInstance(operatorConversions); - binder.bind(DruidSchemaCatalog.class).toInstance(rootSchema); + binder.bind(DruidSchemaCatalogProvider.class) + .toInstance(new ConstantDruidSchemaCatalogProvider(rootSchema)); binder.bind(JoinableFactoryWrapper.class).toInstance(joinableFactoryWrapper); binder.bind(CatalogResolver.class).toInstance(CatalogResolver.NULL_RESOLVER); }, @@ -192,6 +195,7 @@ public void testExtensionCalciteRule() sql, DruidSqlParser.parse(sql, false).getMainStatement(), new NativeSqlEngine(queryLifecycleFactory, mapper, (SqlStatementFactory) null), + null, // Don't need an authentication result Collections.emptySet(), Collections.emptyMap(), null @@ -215,6 +219,7 @@ public void testConfigurableBloat() sql, DruidSqlParser.parse(sql, false).getMainStatement(), new NativeSqlEngine(queryLifecycleFactory, mapper, (SqlStatementFactory) null), + null, // Don't need an authentication result Collections.emptySet(), Collections.singletonMap(BLOAT_PROPERTY, BLOAT), null @@ -225,6 +230,7 @@ public void testConfigurableBloat() sql, DruidSqlParser.parse(sql, false).getMainStatement(), new NativeSqlEngine(queryLifecycleFactory, mapper, (SqlStatementFactory) null), + null, // Don't need an authentication result Collections.emptySet(), Collections.emptyMap(), null diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/planner/DruidRexExecutorTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/planner/DruidRexExecutorTest.java index 2f387bdc125d..3e271f0d3f70 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/planner/DruidRexExecutorTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/planner/DruidRexExecutorTest.java @@ -49,6 +49,7 @@ import org.apache.druid.sql.calcite.expression.OperatorConversions; import org.apache.druid.sql.calcite.expression.builtin.MultiValueStringOperatorConversions; import org.apache.druid.sql.calcite.expression.builtin.TimeParseOperatorConversion; +import org.apache.druid.sql.calcite.schema.ConstantDruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchema; import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; import org.apache.druid.sql.calcite.schema.NamedDruidSchema; @@ -93,11 +94,13 @@ public class DruidRexExecutorTest extends InitializedNullHandlingTest CalciteTests.createExprMacroTable(), CalciteTests.getJsonMapper(), new PlannerConfig(), - new DruidSchemaCatalog( - EasyMock.createMock(SchemaPlus.class), - ImmutableMap.of( - "druid", new NamedDruidSchema(EasyMock.createMock(DruidSchema.class), "druid"), - NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) + new ConstantDruidSchemaCatalogProvider( + new DruidSchemaCatalog( + EasyMock.createMock(SchemaPlus.class), + ImmutableMap.of( + "druid", new NamedDruidSchema(EasyMock.createMock(DruidSchema.class), "druid"), + NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) + ) ) ), CalciteTests.createJoinableFactoryWrapper(), @@ -114,6 +117,7 @@ NamedViewSchema.NAME, new NamedViewSchema(EasyMock.createMock(ViewSchema.class)) "SELECT 1", // The actual query isn't important for this test null, /* Don't need a SQL node */ null, /* Don't need an engine */ + null, /* Don't need an authentication result */ Collections.emptySet(), Collections.emptyMap(), null diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/ConstantDruidSchemaCatalogProvider.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/ConstantDruidSchemaCatalogProvider.java new file mode 100644 index 000000000000..bf5bbbed535f --- /dev/null +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/ConstantDruidSchemaCatalogProvider.java @@ -0,0 +1,49 @@ +/* + * 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.druid.sql.calcite.schema; + +import org.apache.druid.server.security.AuthenticationResult; + +/** + * Implementation of {@link DruidSchemaCatalogProvider} that always returns the same catalog. Not suitable for + * security-related tests because the user catalog and escalated catalog are the same; in a real environment these + * would be different. + */ +public class ConstantDruidSchemaCatalogProvider implements DruidSchemaCatalogProvider +{ + private final DruidSchemaCatalog theCatalog; + + public ConstantDruidSchemaCatalogProvider(DruidSchemaCatalog theCatalog) + { + this.theCatalog = theCatalog; + } + + @Override + public DruidSchemaCatalog createRootSchema(AuthenticationResult authenticationResult) + { + return theCatalog; + } + + @Override + public DruidSchemaCatalog createEscalatedRootSchema() + { + return theCatalog; + } +} diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidCalciteSchemaModuleTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidCalciteSchemaModuleTest.java index e308a0331ef8..347c98f23424 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidCalciteSchemaModuleTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidCalciteSchemaModuleTest.java @@ -27,7 +27,6 @@ import com.google.inject.Key; import com.google.inject.Scopes; import com.google.inject.TypeLiteral; -import com.google.inject.name.Names; import org.apache.druid.catalog.MapMetadataCatalog; import org.apache.druid.catalog.MetadataCatalog; import org.apache.druid.client.FilteredServerInventoryView; @@ -50,6 +49,8 @@ import org.apache.druid.segment.metadata.CentralizedDatasourceSchemaConfig; import org.apache.druid.server.QueryLifecycleFactory; import org.apache.druid.server.SegmentManager; +import org.apache.druid.server.security.AuthTestUtils; +import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Escalator; import org.apache.druid.sql.calcite.planner.CatalogResolver; @@ -66,6 +67,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -73,6 +75,8 @@ public class DruidCalciteSchemaModuleTest extends CalciteTestBase { private static final String DRUID_SCHEMA_NAME = "druid"; + private static final AuthenticationResult AUTH_RESULT = + new AuthenticationResult("identity", "authorizer", "authenticator", null); @Mock private QueryLifecycleFactory queryLifecycleFactory; @@ -85,8 +89,6 @@ public class DruidCalciteSchemaModuleTest extends CalciteTestBase @Mock private Escalator escalator; @Mock - AuthorizerMapper authorizerMapper; - @Mock private FilteredServerInventoryView serverInventoryView; @Mock private DruidNodeDiscoveryProvider druidNodeDiscoveryProvider; @@ -101,15 +103,14 @@ public class DruidCalciteSchemaModuleTest extends CalciteTestBase @Mock private HttpClient httpClient; - private DruidCalciteSchemaModule target; private Injector injector; @BeforeEach public void setUp() { EasyMock.expect(plannerConfig.isEnableSysQueriesTable()).andReturn(false).anyTimes(); + EasyMock.expect(plannerConfig.isAuthorizeTableVisibility()).andReturn(false).anyTimes(); EasyMock.replay(plannerConfig); - target = new DruidCalciteSchemaModule(); injector = Guice.createInjector( binder -> { binder.bind(QueryLifecycleFactory.class).toInstance(queryLifecycleFactory); @@ -118,7 +119,7 @@ public void setUp() binder.bind(PlannerConfig.class).toInstance(plannerConfig); binder.bind(ViewManager.class).toInstance(viewManager); binder.bind(Escalator.class).toInstance(escalator); - binder.bind(AuthorizerMapper.class).toInstance(authorizerMapper); + binder.bind(AuthorizerMapper.class).toInstance(AuthTestUtils.TEST_AUTHORIZER_MAPPER); binder.bind(FilteredServerInventoryView.class).toInstance(serverInventoryView); binder.bind(SegmentManager.class).toInstance(segmentManager); binder.bind(DruidOperatorTable.class).toInstance(druidOperatorTable); @@ -139,7 +140,8 @@ public void setUp() binder.bind(new TypeLiteral>() {}).toInstance(ImmutableSet.of()); }, new LifecycleModule(), - target); + new DruidCalciteSchemaModule() + ); } @Test @@ -150,64 +152,31 @@ public void testDruidSchemaNameIsInjected() } @Test - public void testDruidSqlSchemaIsInjectedAsSingleton() - { - NamedDruidSchema namedDruidSchema = injector.getInstance(NamedDruidSchema.class); - Assertions.assertNotNull(namedDruidSchema); - NamedDruidSchema other = injector.getInstance(NamedDruidSchema.class); - Assertions.assertSame(other, namedDruidSchema); - } - - @Test - public void testSystemSqlSchemaIsInjectedAsSingleton() - { - NamedSystemSchema namedSystemSchema = injector.getInstance(NamedSystemSchema.class); - Assertions.assertNotNull(namedSystemSchema); - NamedSystemSchema other = injector.getInstance(NamedSystemSchema.class); - Assertions.assertSame(other, namedSystemSchema); - } - - @Test - public void testDruidCalciteSchemasAreInjected() + public void testNamedSchemasAreInjected() { - Set sqlSchemas = injector.getInstance(Key.get(new TypeLiteral<>() {})); - Set> expectedSchemas = Set.of( - NamedSystemSchema.class, - NamedDruidSchema.class, - NamedLookupSchema.class, - NamedViewSchema.class - ); - Assertions.assertEquals(expectedSchemas.size(), sqlSchemas.size()); + Set namedSchemas = injector.getInstance(Key.get(new TypeLiteral<>() {})); Assertions.assertEquals( - expectedSchemas, - sqlSchemas.stream().map(NamedSchema::getClass).collect(Collectors.toSet())); - } - - @Test - public void testDruidSchemaIsInjectedAsSingleton() - { - DruidSchema schema = injector.getInstance(DruidSchema.class); - Assertions.assertNotNull(schema); - DruidSchema other = injector.getInstance(DruidSchema.class); - Assertions.assertSame(other, schema); + Set.of(NamedLookupSchema.class), + namedSchemas.stream().map(NamedSchema::getClass).collect(Collectors.toSet()) + ); } @Test - public void testSystemSchemaIsInjectedAsSingleton() + public void testSchemaProvidersAreInjected() { - SystemSchema schema = injector.getInstance(SystemSchema.class); - Assertions.assertNotNull(schema); - SystemSchema other = injector.getInstance(SystemSchema.class); - Assertions.assertSame(other, schema); + Set schemaProviders = injector.getInstance(Key.get(new TypeLiteral<>() {})); + Assertions.assertEquals( + Set.of(DruidSchemaProvider.class, SystemSchemaProvider.class, ViewSchemaProvider.class), + schemaProviders.stream().map(SchemaProvider::getClass).collect(Collectors.toSet()) + ); } @Test - public void testInformationSchemaIsInjectedAsSingleton() + public void testDruidSchemaProviderIsInjectedAsSingleton() { - InformationSchema schema = injector.getInstance(InformationSchema.class); - Assertions.assertNotNull(schema); - InformationSchema other = injector.getInstance(InformationSchema.class); - Assertions.assertSame(other, schema); + DruidSchemaProvider schemaProvider = injector.getInstance(DruidSchemaProvider.class); + Assertions.assertNotNull(schemaProvider); + Assertions.assertSame(schemaProvider, injector.getInstance(DruidSchemaProvider.class)); } @Test @@ -220,35 +189,47 @@ public void testLookupSchemaIsInjectedAsSingleton() } @Test - public void testRootSchemaAnnotatedIsInjectedAsSingleton() + public void testSchemaCatalogProviderIsInjectedAsSingleton() { - DruidSchemaCatalog rootSchema = injector.getInstance( - Key.get(DruidSchemaCatalog.class, Names.named(DruidCalciteSchemaModule.INCOMPLETE_SCHEMA)) - ); - Assertions.assertNotNull(rootSchema); - DruidSchemaCatalog other = injector.getInstance( - Key.get(DruidSchemaCatalog.class, Names.named(DruidCalciteSchemaModule.INCOMPLETE_SCHEMA)) - ); - Assertions.assertSame(other, rootSchema); + DruidSchemaCatalogProvider provider = injector.getInstance(DruidSchemaCatalogProvider.class); + Assertions.assertInstanceOf(DruidSchemaCatalogProviderImpl.class, provider); + Assertions.assertSame(provider, injector.getInstance(DruidSchemaCatalogProvider.class)); } @Test - public void testRootSchemaIsInjectedAsSingleton() + public void testRootSchemaHasAllSchemasPlusInformationSchema() { - DruidSchemaCatalog rootSchema = injector.getInstance(Key.get(DruidSchemaCatalog.class)); - Assertions.assertNotNull(rootSchema); - DruidSchemaCatalog other = injector.getInstance( - Key.get(DruidSchemaCatalog.class, Names.named(DruidCalciteSchemaModule.INCOMPLETE_SCHEMA)) + EasyMock.expect(viewManager.getViews()).andReturn(Map.of()).anyTimes(); + EasyMock.replay(viewManager); + + final DruidSchemaCatalog rootSchema = + injector.getInstance(DruidSchemaCatalogProvider.class).createRootSchema(AUTH_RESULT); + + // Every schema the module binds must be reachable from the root schema. + Assertions.assertEquals( + Set.of( + DRUID_SCHEMA_NAME, + NamedViewSchema.NAME, + NamedSystemSchema.NAME, + NamedLookupSchema.NAME, + InformationSchema.INFORMATION_SCHEMA_NAME + ), + rootSchema.getSubSchemaNames() + ); + Assertions.assertNotNull( + rootSchema.getSubSchema(InformationSchema.INFORMATION_SCHEMA_NAME).unwrap(InformationSchema.class) ); - Assertions.assertSame(other, rootSchema); } @Test - public void testRootSchemaIsInjectedAndHasInformationSchema() + public void testEscalatedRootSchemaUsesEscalator() { - DruidSchemaCatalog rootSchema = injector.getInstance(Key.get(DruidSchemaCatalog.class)); - InformationSchema expectedSchema = injector.getInstance(InformationSchema.class); - Assertions.assertNotNull(rootSchema); - Assertions.assertSame(expectedSchema, rootSchema.getSubSchema("INFORMATION_SCHEMA").unwrap(InformationSchema.class)); + EasyMock.expect(viewManager.getViews()).andReturn(Map.of()).anyTimes(); + EasyMock.replay(viewManager); + EasyMock.expect(escalator.createEscalatedAuthenticationResult()).andReturn(AUTH_RESULT).once(); + EasyMock.replay(escalator); + + Assertions.assertNotNull(injector.getInstance(DruidSchemaCatalogProvider.class).createEscalatedRootSchema()); + EasyMock.verify(escalator); } } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidSchemaNoDataInitTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidSchemaProviderNoDataInitTest.java similarity index 75% rename from sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidSchemaNoDataInitTest.java rename to sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidSchemaProviderNoDataInitTest.java index dd8c30e94e2e..cbb8ece3fc56 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidSchemaNoDataInitTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/DruidSchemaProviderNoDataInitTest.java @@ -21,6 +21,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import org.apache.calcite.schema.lookup.LikePattern; import org.apache.druid.client.InternalQueryConfig; import org.apache.druid.java.util.common.io.Closer; import org.apache.druid.query.QueryRunnerFactoryConglomerate; @@ -33,6 +35,7 @@ import org.apache.druid.server.metrics.NoopServiceEmitter; import org.apache.druid.server.security.NoopEscalator; import org.apache.druid.sql.calcite.planner.CatalogResolver; +import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.util.CalciteTestBase; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.calcite.util.TestTimelineServerView; @@ -41,14 +44,16 @@ import org.junit.jupiter.api.Test; import java.util.Collections; +import java.util.Set; -public class DruidSchemaNoDataInitTest extends CalciteTestBase +public class DruidSchemaProviderNoDataInitTest extends CalciteTestBase { private static final BrokerSegmentMetadataCacheConfig SEGMENT_CACHE_CONFIG_DEFAULT = BrokerSegmentMetadataCacheConfig.create(); @Test public void testInitializationWithNoData() throws Exception { + final NoopEscalator escalator = new NoopEscalator(); try (final Closer closer = Closer.create()) { final QueryRunnerFactoryConglomerate conglomerate = QueryStackTests.createQueryRunnerFactoryConglomerate(closer); final BrokerSegmentMetadataCache cache = new BrokerSegmentMetadataCache( @@ -58,21 +63,34 @@ public void testInitializationWithNoData() throws Exception ), new TestTimelineServerView(Collections.emptyList()), SEGMENT_CACHE_CONFIG_DEFAULT, - new NoopEscalator(), + escalator, new InternalQueryConfig(), new NoopServiceEmitter(), new PhysicalDatasourceMetadataFactory( new MapJoinableFactory(ImmutableSet.of(), ImmutableMap.of()), - new SegmentManager(EasyMock.createMock(SegmentLocalCacheManager.class))), + new SegmentManager(EasyMock.createMock(SegmentLocalCacheManager.class)) + ), null, CentralizedDatasourceSchemaConfig.create() ); cache.start(); cache.awaitInitialization(); - final DruidSchema druidSchema = new DruidSchema(cache, null, CatalogResolver.NULL_RESOLVER); + final DruidSchemaProvider druidSchemaProvider = new DruidSchemaProvider( + CalciteTests.DRUID_SCHEMA_NAME, + cache, + null, + CatalogResolver.NULL_RESOLVER, + new PlannerConfig(), + CalciteTests.TEST_AUTHORIZER_MAPPER + ); - Assertions.assertEquals(ImmutableSet.of(), druidSchema.getTableNames()); + final Set providedDruidTables = + Iterables.getOnlyElement(druidSchemaProvider.getSchemas(escalator.createEscalatedAuthenticationResult())) + .getSchema() + .tables() + .getNames(LikePattern.any()); + Assertions.assertEquals(ImmutableSet.of(), providedDruidTables); } } } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/InformationSchemaTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/InformationSchemaTest.java index 8c540cbd1a6f..31417860a241 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/InformationSchemaTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/InformationSchemaTest.java @@ -34,6 +34,7 @@ import org.apache.calcite.sql.type.OperandTypes; import org.apache.calcite.sql.type.SqlTypeFamily; import org.apache.druid.segment.column.ColumnType; +import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.sql.calcite.BaseCalciteQueryTest; import org.apache.druid.sql.calcite.expression.DirectOperatorConversion; import org.apache.druid.sql.calcite.expression.OperatorConversions; @@ -65,8 +66,10 @@ public class InformationSchemaTest extends BaseCalciteQueryTest @BeforeEach public void setUp() { + final AuthenticationResult authenticationResult = CalciteTests.SUPER_USER_AUTH_RESULT; + qf = queryFramework(); - DruidSchemaCatalog rootSchema = QueryFrameworkUtils.createMockRootSchema( + DruidSchemaCatalog rootSchema = QueryFrameworkUtils.createMockRootSchemaProvider( CalciteTests.INJECTOR, qf.conglomerate(), qf.walker(), @@ -75,12 +78,13 @@ public void setUp() new NoopDruidSchemaManager(), CalciteTests.TEST_AUTHORIZER_MAPPER, CatalogResolver.NULL_RESOLVER - ); + ).createRootSchema(authenticationResult); informationSchema = new InformationSchema( rootSchema, + qf.operatorTable(), CalciteTests.TEST_AUTHORIZER_MAPPER, - qf.operatorTable() + authenticationResult ); } @@ -256,7 +260,7 @@ public QueryProvider getQueryProvider() @Override public Object get(String authorizerName) { - return CalciteTests.SUPER_USER_AUTH_RESULT; + return null; } }; } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/RootSchemaProviderTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/RootSchemaProviderTest.java deleted file mode 100644 index eb67dde69fd3..000000000000 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/RootSchemaProviderTest.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.druid.sql.calcite.schema; - -import com.google.common.collect.ImmutableSet; -import org.apache.calcite.schema.Schema; -import org.apache.druid.java.util.common.ISE; -import org.apache.druid.sql.calcite.util.CalciteTestBase; -import org.easymock.EasyMock; -import org.easymock.EasyMockExtension; -import org.easymock.Mock; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.assertThrows; - -@ExtendWith(EasyMockExtension.class) -public class RootSchemaProviderTest extends CalciteTestBase -{ - private static final String SCHEMA_1 = "SCHEMA_1"; - private static final String SCHEMA_2 = "SCHEMA_2"; - @Mock - private NamedSchema druidSchema1; - @Mock - private NamedSchema druidSchema2; - @Mock - private NamedSchema duplicateSchema1; - @Mock - private Schema schema1; - @Mock - private Schema schema2; - @Mock - private Schema schema3; - private Set druidSchemas; - - private RootSchemaProvider target; - - @BeforeEach - public void setUp() - { - EasyMock.expect(druidSchema1.getSchema()).andStubReturn(schema1); - EasyMock.expect(druidSchema2.getSchema()).andStubReturn(schema2); - EasyMock.expect(duplicateSchema1.getSchema()).andStubReturn(schema3); - EasyMock.expect(druidSchema1.getSchemaName()).andStubReturn(SCHEMA_1); - EasyMock.expect(druidSchema2.getSchemaName()).andStubReturn(SCHEMA_2); - EasyMock.expect(duplicateSchema1.getSchemaName()).andStubReturn(SCHEMA_1); - EasyMock.replay(druidSchema1, druidSchema2, duplicateSchema1); - - druidSchemas = ImmutableSet.of(druidSchema1, druidSchema2); - target = new RootSchemaProvider(druidSchemas); - } - @Test - public void testGetShouldReturnRootSchemaWithProvidedSchemasRegistered() - { - DruidSchemaCatalog rootSchema = target.get(); - Assertions.assertEquals("", rootSchema.getRootSchema().getName()); - Assertions.assertFalse(rootSchema.getRootSchema().isCacheEnabled()); - // metadata schema should not be added - Assertions.assertEquals(druidSchemas.size(), rootSchema.getSubSchemaNames().size()); - - Assertions.assertEquals(schema1, rootSchema.getSubSchema(SCHEMA_1).unwrap(schema1.getClass())); - Assertions.assertEquals(schema2, rootSchema.getSubSchema(SCHEMA_2).unwrap(schema2.getClass())); - } - - @Test - public void testGetWithDuplicateSchemasShouldThrowISE() - { - assertThrows(ISE.class, () -> { - target = new RootSchemaProvider(ImmutableSet.of(druidSchema1, druidSchema2, duplicateSchema1)); - target.get(); - }); - } -} diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java index 837aef7a050c..0a83956c1528 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java @@ -38,7 +38,7 @@ import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexNode; import org.apache.calcite.schema.SchemaPlus; -import org.apache.calcite.schema.Table; +import org.apache.calcite.schema.lookup.LikePattern; import org.apache.calcite.sql.fun.SqlStdOperatorTable; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.druid.client.DruidServer; @@ -58,6 +58,7 @@ import org.apache.druid.discovery.DruidNodeDiscovery; import org.apache.druid.discovery.DruidNodeDiscoveryProvider; import org.apache.druid.discovery.NodeRole; +import org.apache.druid.error.NotYetImplemented; import org.apache.druid.indexer.TaskStatusPlus; import org.apache.druid.indexer.granularity.GranularitySpec; import org.apache.druid.indexer.partitions.DynamicPartitionsSpec; @@ -100,12 +101,12 @@ import org.apache.druid.server.metrics.NoopServiceEmitter; import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthTestUtils; import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.Authorizer; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.NoopEscalator; import org.apache.druid.server.security.ResourceType; -import org.apache.druid.sql.calcite.planner.CatalogResolver; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.run.SqlEngine; import org.apache.druid.sql.calcite.schema.SystemSchema.QueriesTable; @@ -193,7 +194,6 @@ public class SystemSchemaTest extends CalciteTestBase .withRollup(false) .build(); - private SystemSchema schema; private SpecificSegmentsQuerySegmentWalker walker; private CoordinatorClient coordinatorClient; private OverlordClient overlordClient; @@ -201,7 +201,7 @@ public class SystemSchemaTest extends CalciteTestBase private StringFullResponseHolder responseHolder; private BytesAccumulatingResponseHandler responseHandler; private Request request; - private DruidSchema druidSchema; + private BrokerSegmentMetadataCache segmentMetadataCache; private AuthorizerMapper authMapper; private static QueryRunnerFactoryConglomerate conglomerate; private static Closer resourceCloser; @@ -267,7 +267,7 @@ public void setUp(@TempDir File tmpDir) throws Exception .add(segment2, index2) .add(segment3, index3); - BrokerSegmentMetadataCache cache = new BrokerSegmentMetadataCache( + segmentMetadataCache = new BrokerSegmentMetadataCache( CalciteTests.createMockQueryLifecycleFactory(walker, conglomerate), new TestTimelineServerView(walker.getSegments(), realtimeSegments), SEGMENT_CACHE_CONFIG_DEFAULT, @@ -281,27 +281,12 @@ public void setUp(@TempDir File tmpDir) throws Exception new NoopCoordinatorClient(), CentralizedDatasourceSchemaConfig.create() ); - cache.start(); - cache.awaitInitialization(); - druidSchema = new DruidSchema(cache, null, CatalogResolver.NULL_RESOLVER); + segmentMetadataCache.start(); + segmentMetadataCache.awaitInitialization(); metadataView = EasyMock.createMock(MetadataSegmentView.class); druidNodeDiscoveryProvider = EasyMock.createMock(DruidNodeDiscoveryProvider.class); serverInventoryView = EasyMock.createMock(FilteredServerInventoryView.class); httpClient = EasyMock.createMock(HttpClient.class); - schema = new SystemSchema( - druidSchema, - metadataView, - serverView, - serverInventoryView, - EasyMock.createStrictMock(AuthorizerMapper.class), - coordinatorClient, - overlordClient, - druidNodeDiscoveryProvider, - MAPPER, - httpClient, - () -> new SqlEngineRegistry(Collections.emptySet()), - new PlannerConfig() - ); } private final CompactionState expectedCompactionState = @@ -569,25 +554,43 @@ DataNodeService.DISCOVERY_SERVICE_KEY, new DataNodeService("tier", 1000, null, S private final List immutableDruidServers = ImmutableList.of(druidServer1, druidServer2); @Test - public void testGetTableMap() + public void testGetTables() { + final PlannerConfig plannerConfig = new PlannerConfig(); + final SystemSchema schema = new SystemSchema( + segmentMetadataCache, + metadataView, + serverView, + serverInventoryView, + AuthTestUtils.TEST_AUTHORIZER_MAPPER, + coordinatorClient, + overlordClient, + druidNodeDiscoveryProvider, + MAPPER, + httpClient, + () -> new SqlEngineRegistry(Collections.emptySet()), + plannerConfig, + createAuthResult(Users.SUPER), + SystemSchemaProvider.computeAllTableNames(plannerConfig) + ); + Assertions.assertEquals( ImmutableSet.of("segments", "servers", "server_segments", "tasks", "supervisors", "server_properties"), schema.getTableNames() ); - final Map tableMap = schema.getTableMap(); Assertions.assertEquals( ImmutableSet.of("segments", "servers", "server_segments", "tasks", "supervisors", "server_properties"), - tableMap.keySet() + schema.tables().getNames(LikePattern.any()) ); - final SystemSchema.SegmentsTable segmentsTable = (SystemSchema.SegmentsTable) schema.getTableMap().get("segments"); + + final SystemSchema.SegmentsTable segmentsTable = (SystemSchema.SegmentsTable) schema.tables().get("segments"); final RelDataType rowType = segmentsTable.getRowType(new JavaTypeFactoryImpl()); final List fields = rowType.getFieldList(); Assertions.assertEquals(20, fields.size()); - final SystemSchema.TasksTable tasksTable = (SystemSchema.TasksTable) schema.getTableMap().get("tasks"); + final SystemSchema.TasksTable tasksTable = (SystemSchema.TasksTable) schema.tables().get("tasks"); final RelDataType sysRowType = tasksTable.getRowType(new JavaTypeFactoryImpl()); final List sysFields = sysRowType.getFieldList(); Assertions.assertEquals(14, sysFields.size()); @@ -595,14 +598,14 @@ public void testGetTableMap() Assertions.assertEquals("task_id", sysFields.get(0).getName()); Assertions.assertEquals(SqlTypeName.VARCHAR, sysFields.get(0).getType().getSqlTypeName()); - final SystemSchema.ServersTable serversTable = (SystemSchema.ServersTable) schema.getTableMap().get("servers"); + final SystemSchema.ServersTable serversTable = (SystemSchema.ServersTable) schema.tables().get("servers"); final RelDataType serverRowType = serversTable.getRowType(new JavaTypeFactoryImpl()); final List serverFields = serverRowType.getFieldList(); Assertions.assertEquals(16, serverFields.size()); Assertions.assertEquals("server", serverFields.get(0).getName()); Assertions.assertEquals(SqlTypeName.VARCHAR, serverFields.get(0).getType().getSqlTypeName()); - final SystemServerPropertiesTable propertiesTable = (SystemServerPropertiesTable) schema.getTableMap() + final SystemServerPropertiesTable propertiesTable = (SystemServerPropertiesTable) schema.tables() .get("server_properties"); final RelDataType propertiesRowType = propertiesTable.getRowType(new JavaTypeFactoryImpl()); final List propertiesFields = propertiesRowType.getFieldList(); @@ -613,9 +616,9 @@ public void testGetTableMap() public void testSegmentsTableGetDataSourceFilter() { final RexBuilder rexBuilder = new RexBuilder(new JavaTypeFactoryImpl()); - final RexLiteral foo = (RexLiteral) rexBuilder.makeLiteral("foo"); - final RexLiteral bar = (RexLiteral) rexBuilder.makeLiteral("bar"); - final RexLiteral baz = (RexLiteral) rexBuilder.makeLiteral("baz"); + final RexLiteral foo = rexBuilder.makeLiteral("foo"); + final RexLiteral bar = rexBuilder.makeLiteral("bar"); + final RexLiteral baz = rexBuilder.makeLiteral("baz"); // Match the input-ref type to the literal type so Calcite does not wrap the literal in a CAST. // "datasource" is column index 1, "size" is column index 4 in SEGMENTS_SIGNATURE. final RexNode dsRef = rexBuilder.makeInputRef(foo.getType(), 1); @@ -694,7 +697,8 @@ public void testSegmentsTableGetDataSourceFilter() @Test public void testSegmentsTable() throws Exception { - final SegmentsTable segmentsTable = new SegmentsTable(druidSchema, metadataView, MAPPER, authMapper); + final SegmentsTable segmentsTable = + new SegmentsTable(segmentMetadataCache, metadataView, MAPPER, authMapper, createAuthResult(Users.SUPER)); final Set publishedSegments = new HashSet<>(Arrays.asList( new SegmentStatusInCluster(publishedCompactedSegment1, true, 2, null, false), new SegmentStatusInCluster(publishedCompactedSegment2, false, 0, null, false), @@ -706,7 +710,7 @@ public void testSegmentsTable() throws Exception EasyMock.expect(metadataView.getSegments(EasyMock.anyObject())).andReturn(publishedSegments.iterator()).once(); EasyMock.replay(request, responseHolder, responseHandler, metadataView); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = segmentsTable.scan(dataContext, Collections.emptyList(), null).toList(); rows.sort((Object[] row1, Object[] row2) -> ((Comparable) row1[0]).compareTo(row2[0])); @@ -814,7 +818,8 @@ public void testSegmentsTable() throws Exception @Test public void testSegmentsTableWithProjection() throws JsonProcessingException { - final SegmentsTable segmentsTable = new SegmentsTable(druidSchema, metadataView, MAPPER, authMapper); + final SegmentsTable segmentsTable = + new SegmentsTable(segmentMetadataCache, metadataView, MAPPER, authMapper, createAuthResult(Users.SUPER)); final Set publishedSegments = new HashSet<>(Arrays.asList( new SegmentStatusInCluster(publishedCompactedSegment1, true, 2, null, false), new SegmentStatusInCluster(publishedCompactedSegment2, false, 0, null, false), @@ -826,7 +831,7 @@ public void testSegmentsTableWithProjection() throws JsonProcessingException EasyMock.expect(metadataView.getSegments(EasyMock.anyObject())).andReturn(publishedSegments.iterator()).once(); EasyMock.replay(request, responseHolder, responseHandler, metadataView); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = segmentsTable.scan( dataContext, Collections.emptyList(), @@ -890,7 +895,8 @@ public void testServersTable() throws URISyntaxException authMapper, overlordClient, coordinatorClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ) .createMock(); EasyMock.replay(serversTable); @@ -968,7 +974,7 @@ public void testServersTable() throws URISyntaxException indexerNodeDiscovery ); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = serversTable.scan(dataContext).toList(); rows.sort((Object[] row1, Object[] row2) -> ((Comparable) row1[0]).compareTo(row2[0])); @@ -1342,14 +1348,14 @@ public void testServerSegmentsTable() { SystemSchema.ServerSegmentsTable serverSegmentsTable = EasyMock .createMockBuilder(SystemSchema.ServerSegmentsTable.class) - .withConstructor(serverView, authMapper) + .withConstructor(serverView, authMapper, createAuthResult(Users.SUPER)) .createMock(); EasyMock.replay(serverSegmentsTable); EasyMock.expect(serverView.getDruidServers()) .andReturn(immutableDruidServers) .once(); EasyMock.replay(serverView); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); //server_segments table is the join of servers and segments table // it will have 5 rows as follows @@ -1390,9 +1396,10 @@ public void testServerSegmentsTable() public void testTasksTable() throws Exception { - SystemSchema.TasksTable tasksTable = EasyMock.createMockBuilder(SystemSchema.TasksTable.class) - .withConstructor(overlordClient, authMapper) - .createMock(); + SystemSchema.TasksTable tasksTable = + EasyMock.createMockBuilder(SystemSchema.TasksTable.class) + .withConstructor(overlordClient, authMapper, createAuthResult(Users.SUPER)) + .createMock(); EasyMock.replay(tasksTable); @@ -1439,7 +1446,7 @@ public void testTasksTable() throws Exception ); EasyMock.replay(overlordClient, request, responseHandler); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = tasksTable.scan(dataContext).toList(); Object[] row0 = rows.get(0); @@ -1481,8 +1488,6 @@ public void testTasksTable() throws Exception @Test public void testTasksTableAuth() { - SystemSchema.TasksTable tasksTable = new SystemSchema.TasksTable(overlordClient, authMapper); - String json = "[{\n" + "\t\"id\": \"index_wikipedia_2018-09-20T22:33:44.911Z\",\n" + "\t\"groupId\": \"group_index_wikipedia_2018-09-20T22:33:44.911Z\",\n" @@ -1528,21 +1533,24 @@ public void testTasksTableAuth() EasyMock.replay(overlordClient); // Verify that no row is returned for Datasource Write user - List rows = tasksTable - .scan(createDataContext(Users.DATASOURCE_WRITE)) - .toList(); + List rows = + new SystemSchema.TasksTable(overlordClient, authMapper, createAuthResult(Users.DATASOURCE_WRITE)) + .scan(createDataContext()) + .toList(); Assertions.assertTrue(rows.isEmpty()); // Verify that 2 rows are returned for Datasource Read user - rows = tasksTable - .scan(createDataContext(Users.DATASOURCE_READ)) - .toList(); + rows = + new SystemSchema.TasksTable(overlordClient, authMapper, createAuthResult(Users.DATASOURCE_READ)) + .scan(createDataContext()) + .toList(); Assertions.assertEquals(2, rows.size()); // Verify that 2 rows are returned for Super user - rows = tasksTable - .scan(createDataContext(Users.SUPER)) - .toList(); + rows = + new SystemSchema.TasksTable(overlordClient, authMapper, createAuthResult(Users.SUPER)) + .scan(createDataContext()) + .toList(); Assertions.assertEquals(2, rows.size()); } @@ -1551,7 +1559,7 @@ public void testSupervisorTable() throws Exception { SystemSchema.SupervisorsTable supervisorTable = EasyMock.createMockBuilder(SystemSchema.SupervisorsTable.class) - .withConstructor(overlordClient, authMapper) + .withConstructor(overlordClient, authMapper, createAuthResult(Users.SUPER)) .createMock(); EasyMock.replay(supervisorTable); @@ -1577,7 +1585,7 @@ public void testSupervisorTable() throws Exception ); EasyMock.replay(overlordClient); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = supervisorTable.scan(dataContext).toList(); Object[] row0 = rows.get(0); @@ -1601,9 +1609,6 @@ public void testSupervisorTable() throws Exception @Test public void testSupervisorTableAuth() { - SystemSchema.SupervisorsTable supervisorTable = - new SystemSchema.SupervisorsTable(overlordClient, createAuthMapper()); - String json = "[{\n" + "\t\"id\": \"wikipedia_supervisor\",\n" + "\t\"dataSource\": \"wikipedia\",\n" @@ -1628,21 +1633,24 @@ public void testSupervisorTableAuth() EasyMock.replay(overlordClient); // Verify that no row is returned for Datasource Write user - List rows = supervisorTable - .scan(createDataContext(Users.DATASOURCE_WRITE)) - .toList(); + List rows = + new SystemSchema.SupervisorsTable(overlordClient, authMapper, createAuthResult(Users.DATASOURCE_WRITE)) + .scan(createDataContext()) + .toList(); Assertions.assertTrue(rows.isEmpty()); - // Verify that 1 row is returned for Datasource Write user - rows = supervisorTable - .scan(createDataContext(Users.DATASOURCE_READ)) - .toList(); + // Verify that 1 row is returned for Datasource Read user + rows = + new SystemSchema.SupervisorsTable(overlordClient, authMapper, createAuthResult(Users.DATASOURCE_READ)) + .scan(createDataContext()) + .toList(); Assertions.assertEquals(1, rows.size()); // Verify that 1 row is returned for Super user - rows = supervisorTable - .scan(createDataContext(Users.SUPER)) - .toList(); + rows = + new SystemSchema.SupervisorsTable(overlordClient, authMapper, createAuthResult(Users.SUPER)) + .scan(createDataContext()) + .toList(); Assertions.assertEquals(1, rows.size()); // TODO: If needed, verify the first row here @@ -1654,9 +1662,16 @@ public void testSupervisorTableAuth() @Test public void testPropertiesTable() { - SystemServerPropertiesTable propertiesTable = EasyMock.createMockBuilder(SystemServerPropertiesTable.class) - .withConstructor(druidNodeDiscoveryProvider, authMapper, httpClient, MAPPER) - .createMock(); + SystemServerPropertiesTable propertiesTable = + EasyMock.createMockBuilder(SystemServerPropertiesTable.class) + .withConstructor( + druidNodeDiscoveryProvider, + authMapper, + httpClient, + MAPPER, + createAuthResult(Users.SUPER) + ) + .createMock(); EasyMock.replay(propertiesTable); @@ -1748,7 +1763,7 @@ public void testPropertiesTable() EasyMock.replay(druidNodeDiscoveryProvider, responseHandler, httpClient); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = propertiesTable.scan(dataContext, Collections.emptyList(), null).toList(); expectedRows.sort((Object[] row1, Object[] row2) -> ((Comparable) row1[0]).compareTo(row2[0])); rows.sort((Object[] row1, Object[] row2) -> ((Comparable) row1[0]).compareTo(row2[0])); @@ -1766,7 +1781,8 @@ public void testPropertiesTable_withUnreachableServer() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); mockAllNodeRolesWithCoordinator(coordinator); @@ -1781,7 +1797,7 @@ public void testPropertiesTable_withUnreachableServer() EasyMock.replay(druidNodeDiscoveryProvider, httpClient); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = propertiesTable.scan(dataContext, Collections.emptyList(), null).toList(); // Should return 1 row even though properties fetch failed @@ -1810,7 +1826,8 @@ public void testPropertiesTable_withHttpError() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); mockAllNodeRolesWithCoordinator(coordinator); @@ -1829,7 +1846,7 @@ public void testPropertiesTable_withHttpError() EasyMock.replay(druidNodeDiscoveryProvider, httpClient); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = propertiesTable.scan(dataContext, Collections.emptyList(), null).toList(); Assertions.assertEquals(1, rows.size()); @@ -1849,7 +1866,8 @@ public void testPropertiesTable_filterPushdown() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); mockAllNodeRolesWithCoordinator(coordinator, coordinator2); @@ -1875,7 +1893,7 @@ public void testPropertiesTable_filterPushdown() rexBuilder.makeLiteral("localhost:8081") ); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = propertiesTable.scan(dataContext, ImmutableList.of(serverEquality), null).toList(); Assertions.assertEquals(1, rows.size()); @@ -1893,7 +1911,8 @@ public void testPropertiesTable_filterPushdownInFilter() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); mockAllNodeRolesWithCoordinator(coordinator, coordinator2); @@ -1918,7 +1937,7 @@ public void testPropertiesTable_filterPushdownInFilter() ); final List rows = - propertiesTable.scan(createDataContext(Users.SUPER), ImmutableList.of(serverIn), null).toList(); + propertiesTable.scan(createDataContext(), ImmutableList.of(serverIn), null).toList(); Assertions.assertEquals(1, rows.size()); Assertions.assertEquals("localhost:8081", rows.get(0)[0]); @@ -1933,7 +1952,8 @@ public void testPropertiesTable_filterPushdownServiceNameAndNonMatching() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); mockAllNodeRolesWithCoordinator(coordinator, coordinator2); @@ -1971,7 +1991,7 @@ public void testPropertiesTable_filterPushdownServiceNameAndNonMatching() rexBuilder.makeLiteral("s1") ); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); List rows = propertiesTable.scan(dataContext, ImmutableList.of(serviceNameEquality), null).toList(); Assertions.assertEquals(2, rows.size()); @@ -1987,7 +2007,7 @@ public void testPropertiesTable_filterPushdownServiceNameAndNonMatching() rexBuilder.makeLiteral("nonexistent:9999") ); - dataContext = createDataContext(Users.SUPER); + dataContext = createDataContext(); rows = propertiesTable.scan(dataContext, ImmutableList.of(nonMatchingFilter), null).toList(); Assertions.assertEquals(0, rows.size()); @@ -2001,7 +2021,8 @@ public void testPropertiesTable_filterFallback() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); final RexBuilder rexBuilder = new RexBuilder(new JavaTypeFactoryImpl()); @@ -2021,7 +2042,7 @@ public void testPropertiesTable_filterFallback() rexBuilder.makeInputRef(rowType.getFieldList().get(SERVER_INDEX).getType(), SERVER_INDEX), rexBuilder.makeLiteral("some-server:1234") ); - Assertions.assertEquals(2, propertiesTable.scan(createDataContext(Users.SUPER), ImmutableList.of(notEquals), null).toList().size()); + Assertions.assertEquals(2, propertiesTable.scan(createDataContext(), ImmutableList.of(notEquals), null).toList().size()); EasyMock.verify(druidNodeDiscoveryProvider, httpClient); // 2) Non-RexCall filter (bare RexInputRef) is ignored @@ -2035,7 +2056,7 @@ public void testPropertiesTable_filterFallback() EasyMock.replay(druidNodeDiscoveryProvider, httpClient); final RexNode inputRef = rexBuilder.makeInputRef(rowType.getFieldList().get(SERVER_INDEX).getType(), SERVER_INDEX); - Assertions.assertEquals(2, propertiesTable.scan(createDataContext(Users.SUPER), ImmutableList.of(inputRef), null).toList().size()); + Assertions.assertEquals(2, propertiesTable.scan(createDataContext(), ImmutableList.of(inputRef), null).toList().size()); EasyMock.verify(druidNodeDiscoveryProvider, httpClient); // 3) Equality on non-pushed column (property) is ignored @@ -2053,7 +2074,7 @@ public void testPropertiesTable_filterFallback() rexBuilder.makeInputRef(rowType.getFieldList().get(PROPERTY_INDEX).getType(), PROPERTY_INDEX), rexBuilder.makeLiteral("druid.key") ); - Assertions.assertEquals(2, propertiesTable.scan(createDataContext(Users.SUPER), ImmutableList.of(propertyEquality), null).toList().size()); + Assertions.assertEquals(2, propertiesTable.scan(createDataContext(), ImmutableList.of(propertyEquality), null).toList().size()); EasyMock.verify(druidNodeDiscoveryProvider, httpClient); // 4) Reversed equality ('localhost:8081' = server) is correctly extracted @@ -2071,7 +2092,7 @@ public void testPropertiesTable_filterFallback() rexBuilder.makeLiteral("localhost:8081"), rexBuilder.makeInputRef(rowType.getFieldList().get(SERVER_INDEX).getType(), SERVER_INDEX) ); - List rows = propertiesTable.scan(createDataContext(Users.SUPER), ImmutableList.of(reversedEquality), null).toList(); + List rows = propertiesTable.scan(createDataContext(), ImmutableList.of(reversedEquality), null).toList(); Assertions.assertEquals(2, rows.size()); Assertions.assertEquals("localhost:8081", rows.get(0)[0]); EasyMock.verify(druidNodeDiscoveryProvider, httpClient); @@ -2084,7 +2105,8 @@ public void testPropertiesTable_projectionAndMultiRole() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); // Same host:port under two roles @@ -2120,7 +2142,7 @@ public void testPropertiesTable_projectionAndMultiRole() EasyMock.replay(druidNodeDiscoveryProvider, httpClient); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); // Multi-role: only 1 HTTP call, node_roles contains both final List fullRows = propertiesTable.scan(dataContext, Collections.emptyList(), null).toList(); @@ -2158,7 +2180,8 @@ public void testPropertiesTable_withInterruptedException() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); mockAllNodeRolesWithCoordinator(coordinator); @@ -2171,7 +2194,7 @@ public void testPropertiesTable_withInterruptedException() EasyMock.replay(druidNodeDiscoveryProvider, httpClient); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); Thread.currentThread().interrupt(); RuntimeException ex = Assertions.assertThrows( RuntimeException.class, @@ -2191,7 +2214,8 @@ public void testPropertiesTable_exceptionWithNullMessage() druidNodeDiscoveryProvider, authMapper, httpClient, - MAPPER + MAPPER, + createAuthResult(Users.SUPER) ); mockAllNodeRolesWithCoordinator(coordinator); @@ -2203,7 +2227,7 @@ public void testPropertiesTable_exceptionWithNullMessage() EasyMock.replay(druidNodeDiscoveryProvider, httpClient); - DataContext dataContext = createDataContext(Users.SUPER); + DataContext dataContext = createDataContext(); final List rows = propertiesTable.scan(dataContext, Collections.emptyList(), null).toList(); Assertions.assertEquals(1, rows.size()); @@ -2230,10 +2254,10 @@ public void testQueriesTable() EasyMock.replay(mockEngine); final SqlEngineRegistry registry = new SqlEngineRegistry(ImmutableSet.of(mockEngine)); - final QueriesTable queriesTable = new QueriesTable(() -> registry, MAPPER, authMapper); + final QueriesTable queriesTable = + new QueriesTable(() -> registry, MAPPER, authMapper, createAuthResult(Users.SUPER)); - final DataContext dataContext = createDataContext(Users.SUPER); - final List rows = queriesTable.scan(dataContext, Collections.emptyList(), null).toList(); + final List rows = queriesTable.scan(createDataContext(), Collections.emptyList(), null).toList(); Assertions.assertEquals(2, rows.size()); @@ -2256,10 +2280,13 @@ public void testQueriesTable() } @Test - public void testSupervisorTableAuthOnDataSourceName() throws JsonProcessingException + public void testSupervisorTableAuthOnDataSourceName() { - SystemSchema.SupervisorsTable supervisorTable = - new SystemSchema.SupervisorsTable(overlordClient, createAuthMapper()); + SystemSchema.SupervisorsTable supervisorTable = new SystemSchema.SupervisorsTable( + overlordClient, + createAuthMapper(), + createAuthResult(Users.ONLY_DATASOURCE_ALL_ACCESS) + ); // Verify that 1 row is returned for datasource name DATASOURCE_ALL_ACCESS String datasourceAllAccessSupervisor = @@ -2284,7 +2311,7 @@ public void testSupervisorTableAuthOnDataSourceName() throws JsonProcessingExcep ).times(1); EasyMock.replay(overlordClient); List rows = supervisorTable - .scan(createDataContext(Users.ONLY_DATASOURCE_ALL_ACCESS)) + .scan(createDataContext()) .toList(); Assertions.assertEquals(1, rows.size()); EasyMock.verify(overlordClient); @@ -2313,7 +2340,7 @@ public void testSupervisorTableAuthOnDataSourceName() throws JsonProcessingExcep ).times(1); EasyMock.replay(overlordClient); rows = supervisorTable - .scan(createDataContext(Users.ONLY_DATASOURCE_ALL_ACCESS)) + .scan(createDataContext()) .toList(); Assertions.assertTrue(rows.isEmpty()); EasyMock.verify(overlordClient); @@ -2342,7 +2369,7 @@ public void testSupervisorTableAuthOnDataSourceName() throws JsonProcessingExcep ).times(1); EasyMock.replay(overlordClient); rows = supervisorTable - .scan(createDataContext(Users.ONLY_DATASOURCE_ALL_ACCESS)) + .scan(createDataContext()) .toList(); Assertions.assertTrue(rows.isEmpty()); EasyMock.verify(overlordClient); @@ -2401,7 +2428,7 @@ private InputStreamFullResponseHolder createFullResponseHolder( /** * Creates a DataContext for the given username. */ - private DataContext createDataContext(String username) + private DataContext createDataContext() { return new DataContext() { @@ -2424,11 +2451,9 @@ public QueryProvider getQueryProvider() } @Override - public Object get(String authorizerName) + public Object get(String name) { - return CalciteTests.TEST_SUPERUSER_NAME.equals(username) - ? CalciteTests.SUPER_USER_AUTH_RESULT - : new AuthenticationResult(username, authorizerName, null, null); + throw NotYetImplemented.ex(null, "Not expected to be called"); } }; } @@ -2461,6 +2486,13 @@ public Authorizer getAuthorizer(String name) }; } + private AuthenticationResult createAuthResult(final String username) + { + return CalciteTests.TEST_SUPERUSER_NAME.equals(username) + ? CalciteTests.SUPER_USER_AUTH_RESULT + : new AuthenticationResult(username, "testAuthorizer", null, null); + } + private static void verifyTypes(final List rows, final RowSignature signature) { final RelDataType rowType = RowSignatures.toRelDataType(signature, new JavaTypeFactoryImpl()); diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java index b0e72b2af9c9..d9731ac4d014 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java @@ -84,11 +84,11 @@ import org.apache.druid.sql.calcite.planner.DruidOperatorTable; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.run.NativeSqlEngine; +import org.apache.druid.sql.calcite.schema.BrokerSegmentMetadataCache; import org.apache.druid.sql.calcite.schema.BrokerSegmentMetadataCacheConfig; -import org.apache.druid.sql.calcite.schema.DruidSchema; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.MetadataSegmentView; -import org.apache.druid.sql.calcite.schema.SystemSchema; +import org.apache.druid.sql.calcite.schema.SystemSchemaProvider; import org.apache.druid.sql.calcite.util.testoperator.CalciteTestOperatorModule; import org.apache.druid.sql.http.SqlEngineRegistry; import org.apache.druid.timeline.DataSegment; @@ -122,6 +122,7 @@ public class CalciteTests public static final String ARRAYS_DATASOURCE = "arrays"; public static final String BROADCAST_DATASOURCE = "broadcast"; public static final String FORBIDDEN_DATASOURCE = "forbiddenDatasource"; + public static final String READ_ONLY_DATASOURCE = "readOnlyDatasource"; public static final String RESTRICTED_DATASOURCE = "restrictedDatasource_m1_is_6"; public static final String RESTRICTED_BROADCAST_DATASOURCE = "restrictedBroadcastDatasource_m1_is_6"; public static final String FORBIDDEN_DESTINATION = "forbiddenDestination"; @@ -328,19 +329,11 @@ NodeRole.COORDINATOR, new FakeDruidNodeDiscovery(ImmutableMap.of(NodeRole.COORDI return provider; } - public static SystemSchema createMockSystemSchema( - final DruidSchema druidSchema, - final SpecificSegmentsQuerySegmentWalker walker, - final AuthorizerMapper authorizerMapper - ) - { - return createMockSystemSchema(druidSchema, new TestTimelineServerView(walker.getSegments()), authorizerMapper); - } - - public static SystemSchema createMockSystemSchema( - final DruidSchema druidSchema, + public static SystemSchemaProvider createMockSystemSchemaProvider( + final BrokerSegmentMetadataCache segmentMetadataCache, final TimelineServerView timelineServerView, - final AuthorizerMapper authorizerMapper + final AuthorizerMapper authorizerMapper, + final PlannerConfig plannerConfig ) { final DruidNode coordinatorNode = mockCoordinatorNode(); @@ -408,8 +401,8 @@ private TaskStatusPlus createTaskStatus(String id, String datasource, Long durat } }; - return new SystemSchema( - druidSchema, + return new SystemSchemaProvider( + segmentMetadataCache, new MetadataSegmentView( coordinatorClient, new BrokerSegmentWatcherConfig(), @@ -425,18 +418,18 @@ private TaskStatusPlus createTaskStatus(String id, String datasource, Long durat getJsonMapper(), new FakeHttpClient(), () -> new SqlEngineRegistry(Collections.emptySet()), - new PlannerConfig() + plannerConfig ); } - public static DruidSchemaCatalog createMockRootSchema( + public static DruidSchemaCatalogProvider createMockRootSchemaProvider( final QueryRunnerFactoryConglomerate conglomerate, final SpecificSegmentsQuerySegmentWalker walker, final PlannerConfig plannerConfig, final AuthorizerMapper authorizerMapper ) { - return QueryFrameworkUtils.createMockRootSchema( + return QueryFrameworkUtils.createMockRootSchemaProvider( INJECTOR, conglomerate, walker, diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java index b48743a6d258..c15923bc4564 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java @@ -23,8 +23,6 @@ import com.google.common.collect.ImmutableSet; import com.google.inject.Injector; import org.apache.calcite.avatica.remote.TypedValue; -import org.apache.calcite.jdbc.CalciteSchema; -import org.apache.calcite.schema.SchemaPlus; import org.apache.druid.client.InternalQueryConfig; import org.apache.druid.client.TimelineServerView; import org.apache.druid.query.DefaultGenericQueryMetricsFactory; @@ -59,20 +57,18 @@ import org.apache.druid.sql.calcite.run.SqlEngine; import org.apache.druid.sql.calcite.schema.BrokerSegmentMetadataCache; import org.apache.druid.sql.calcite.schema.BrokerSegmentMetadataCacheConfig; -import org.apache.druid.sql.calcite.schema.DruidSchema; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProviderImpl; import org.apache.druid.sql.calcite.schema.DruidSchemaManager; -import org.apache.druid.sql.calcite.schema.InformationSchema; +import org.apache.druid.sql.calcite.schema.DruidSchemaProvider; import org.apache.druid.sql.calcite.schema.LookupSchema; -import org.apache.druid.sql.calcite.schema.NamedDruidSchema; import org.apache.druid.sql.calcite.schema.NamedLookupSchema; import org.apache.druid.sql.calcite.schema.NamedSchema; -import org.apache.druid.sql.calcite.schema.NamedSystemSchema; -import org.apache.druid.sql.calcite.schema.NamedViewSchema; import org.apache.druid.sql.calcite.schema.NoopDruidSchemaManager; import org.apache.druid.sql.calcite.schema.PhysicalDatasourceMetadataFactory; -import org.apache.druid.sql.calcite.schema.SystemSchema; -import org.apache.druid.sql.calcite.schema.ViewSchema; +import org.apache.druid.sql.calcite.schema.SchemaProvider; +import org.apache.druid.sql.calcite.schema.SystemSchemaProvider; +import org.apache.druid.sql.calcite.schema.ViewSchemaProvider; import org.apache.druid.sql.calcite.view.ViewManager; import org.easymock.EasyMock; @@ -81,11 +77,9 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; public class QueryFrameworkUtils { - public static final String INFORMATION_SCHEMA_NAME = "INFORMATION_SCHEMA"; public static QueryLifecycleFactory createMockQueryLifecycleFactory( final QuerySegmentWalker walker, @@ -144,7 +138,7 @@ private static SqlToolbox createTestToolbox(SqlEngine engine, PlannerFactory pla ); } - public static DruidSchemaCatalog createMockRootSchema( + public static DruidSchemaCatalogProvider createMockRootSchemaProvider( final Injector injector, final QueryRunnerFactoryConglomerate conglomerate, final SpecificSegmentsQuerySegmentWalker walker, @@ -154,8 +148,7 @@ public static DruidSchemaCatalog createMockRootSchema( final AuthorizerMapper authorizerMapper, final CatalogResolver catalogResolver) { - TimelineServerView timelineServerView = new TestTimelineServerView(walker.getSegments()); - return createMockRootSchema( + return createMockRootSchemaProvider( injector, conglomerate, walker, @@ -164,11 +157,11 @@ public static DruidSchemaCatalog createMockRootSchema( druidSchemaManager, authorizerMapper, catalogResolver, - timelineServerView + new TestTimelineServerView(walker.getSegments()) ); } - public static DruidSchemaCatalog createMockRootSchema( + public static DruidSchemaCatalogProvider createMockRootSchemaProvider( final Injector injector, final QueryRunnerFactoryConglomerate conglomerate, final SpecificSegmentsQuerySegmentWalker walker, @@ -180,76 +173,66 @@ public static DruidSchemaCatalog createMockRootSchema( final TimelineServerView timelineServerView ) { - DruidSchema druidSchema = createMockSchema( + DruidSchemaProvider druidSchemaProvider = createMockSchemaProvider( injector, conglomerate, walker, druidSchemaManager, + plannerConfig, + authorizerMapper, catalogResolver, timelineServerView ); - SystemSchema systemSchema = - CalciteTests.createMockSystemSchema(druidSchema, timelineServerView, authorizerMapper); + SystemSchemaProvider systemSchemaProvider = CalciteTests.createMockSystemSchemaProvider( + druidSchemaProvider.getSegmentMetadataCache(), + timelineServerView, + authorizerMapper, + plannerConfig + ); LookupSchema lookupSchema = createMockLookupSchema(injector); DruidOperatorTable createOperatorTable = createOperatorTable(injector); - return createMockRootSchema( - plannerConfig, + return createMockRootSchemaProvider( viewManager, authorizerMapper, - druidSchema, - systemSchema, + druidSchemaProvider, + systemSchemaProvider, lookupSchema, - createOperatorTable + createOperatorTable, + plannerConfig ); } - public static DruidSchemaCatalog createMockRootSchema( - final PlannerConfig plannerConfig, + public static DruidSchemaCatalogProvider createMockRootSchemaProvider( final ViewManager viewManager, final AuthorizerMapper authorizerMapper, - DruidSchema druidSchema, - SystemSchema systemSchema, - LookupSchema lookupSchema, - DruidOperatorTable createOperatorTable + final DruidSchemaProvider druidSchemaProvider, + final SystemSchemaProvider systemSchemaProvider, + final LookupSchema lookupSchema, + final DruidOperatorTable createOperatorTable, + final PlannerConfig plannerConfig ) { - ViewSchema viewSchema = viewManager != null ? new ViewSchema(viewManager) : null; - - SchemaPlus rootSchema = CalciteSchema.createRootSchema(false, false).plus(); - Set namedSchemas = new HashSet<>(); - namedSchemas.add(new NamedDruidSchema(druidSchema, CalciteTests.DRUID_SCHEMA_NAME)); - namedSchemas.add(new NamedSystemSchema(plannerConfig, systemSchema)); - namedSchemas.add(new NamedLookupSchema(lookupSchema)); + final Set namedSchemas = Set.of(new NamedLookupSchema(lookupSchema)); - if (viewSchema != null) { - namedSchemas.add(new NamedViewSchema(viewSchema)); + final Set schemaProviders = new HashSet<>(); + schemaProviders.add(druidSchemaProvider); + schemaProviders.add(systemSchemaProvider); + if (viewManager != null) { + schemaProviders.add(new ViewSchemaProvider(viewManager, authorizerMapper, plannerConfig)); } - DruidSchemaCatalog catalog = new DruidSchemaCatalog( - rootSchema, - namedSchemas.stream().collect(Collectors.toMap(NamedSchema::getSchemaName, x -> x)) + return new DruidSchemaCatalogProviderImpl( + namedSchemas, + schemaProviders, + createOperatorTable, + authorizerMapper, + CalciteTests.TEST_AUTHENTICATOR_ESCALATOR ); - InformationSchema informationSchema = - new InformationSchema( - catalog, - authorizerMapper, - createOperatorTable - ); - rootSchema.add(CalciteTests.DRUID_SCHEMA_NAME, druidSchema); - rootSchema.add(INFORMATION_SCHEMA_NAME, informationSchema); - rootSchema.add(NamedSystemSchema.NAME, systemSchema); - rootSchema.add(NamedLookupSchema.NAME, lookupSchema); - - if (viewSchema != null) { - rootSchema.add(NamedViewSchema.NAME, viewSchema); - } - - return catalog; } - public static DruidSchemaCatalog createMockRootSchema( + public static DruidSchemaCatalogProvider createMockRootSchemaProvider( final Injector injector, final QueryRunnerFactoryConglomerate conglomerate, final SpecificSegmentsQuerySegmentWalker walker, @@ -257,7 +240,7 @@ public static DruidSchemaCatalog createMockRootSchema( final AuthorizerMapper authorizerMapper ) { - return createMockRootSchema( + return createMockRootSchemaProvider( injector, conglomerate, walker, @@ -269,11 +252,13 @@ public static DruidSchemaCatalog createMockRootSchema( ); } - public static DruidSchema createMockSchema( + public static DruidSchemaProvider createMockSchemaProvider( final Injector injector, final QueryRunnerFactoryConglomerate conglomerate, final SpecificSegmentsQuerySegmentWalker walker, final DruidSchemaManager druidSchemaManager, + final PlannerConfig plannerConfig, + final AuthorizerMapper authorizerMapper, final CatalogResolver catalog, final TimelineServerView timelineServerView ) @@ -309,7 +294,14 @@ public Set getDataSourceNames() } cache.stop(); - return new DruidSchema(cache, druidSchemaManager, catalog); + return new DruidSchemaProvider( + CalciteTests.DRUID_SCHEMA_NAME, + cache, + druidSchemaManager, + catalog, + plannerConfig, + authorizerMapper + ); } public static JoinableFactory createDefaultJoinableFactory(Injector injector) @@ -369,6 +361,7 @@ protected DruidPlanner createPlanner() engine, queryPlus.sql(), queryPlus.sqlNode(), + queryPlus.authResult(), queryPlus.authContextKeys(), queryContext, hook @@ -389,6 +382,7 @@ protected DruidPlanner getPlanner() engine, queryPlus.sql(), queryPlus.sqlNode(), + queryPlus.authResult(), queryPlus.authContextKeys(), queryContext, hook diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/SqlTestFramework.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/SqlTestFramework.java index ce56ed4155a1..6346e0691462 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/SqlTestFramework.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/SqlTestFramework.java @@ -110,12 +110,12 @@ import org.apache.druid.sql.calcite.rule.ExtensionCalciteRuleProvider; import org.apache.druid.sql.calcite.run.NativeSqlEngine; import org.apache.druid.sql.calcite.run.SqlEngine; -import org.apache.druid.sql.calcite.schema.DruidSchema; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.schema.DruidSchemaManager; +import org.apache.druid.sql.calcite.schema.DruidSchemaProvider; import org.apache.druid.sql.calcite.schema.LookupSchema; import org.apache.druid.sql.calcite.schema.NoopDruidSchemaManager; -import org.apache.druid.sql.calcite.schema.SystemSchema; +import org.apache.druid.sql.calcite.schema.SystemSchemaProvider; import org.apache.druid.sql.calcite.util.datasets.TestDataSet; import org.apache.druid.sql.calcite.view.DruidViewMacroFactory; import org.apache.druid.sql.calcite.view.InProcessViewManager; @@ -507,19 +507,23 @@ ViewManager createViewManager(Builder builder) @Provides @LazySingleton - private DruidSchema makeDruidSchema( + private DruidSchemaProvider makeDruidSchemaProvider( final Injector injector, QueryRunnerFactoryConglomerate conglomerate, QuerySegmentWalker walker, Builder builder, + PlannerConfig plannerConfig, + AuthorizerMapper authorizerMapper, TimelineServerView timelineServerView ) { - return QueryFrameworkUtils.createMockSchema( + return QueryFrameworkUtils.createMockSchemaProvider( injector, conglomerate, (SpecificSegmentsQuerySegmentWalker) walker, builder.componentSupplier.getPlannerComponentSupplier().createSchemaManager(), + plannerConfig, + authorizerMapper, builder.catalogResolver, timelineServerView ); @@ -527,12 +531,19 @@ private DruidSchema makeDruidSchema( @Provides @LazySingleton - private SystemSchema makeSystemSchema( + private SystemSchemaProvider makeSystemSchema( + DruidSchemaProvider druidSchemaProvider, + TimelineServerView timelineServerView, AuthorizerMapper authorizerMapper, - DruidSchema druidSchema, - TimelineServerView timelineServerView) + PlannerConfig plannerConfig + ) { - return CalciteTests.createMockSystemSchema(druidSchema, timelineServerView, authorizerMapper); + return CalciteTests.createMockSystemSchemaProvider( + druidSchemaProvider.getSegmentMetadataCache(), + timelineServerView, + authorizerMapper, + plannerConfig + ); } @Provides @@ -551,26 +562,25 @@ private LookupSchema makeLookupSchema(final Injector injector) @Provides @LazySingleton - private DruidSchemaCatalog makeCatalog( - final PlannerConfig plannerConfig, + private DruidSchemaCatalogProvider makeCatalogProvider( final ViewManager viewManager, AuthorizerMapper authorizerMapper, - DruidSchema druidSchema, - SystemSchema systemSchema, + DruidSchemaProvider druidSchemaProvider, + SystemSchemaProvider systemSchemaProvider, LookupSchema lookupSchema, - DruidOperatorTable createOperatorTable + DruidOperatorTable createOperatorTable, + PlannerConfig plannerConfig ) { - final DruidSchemaCatalog rootSchema = QueryFrameworkUtils.createMockRootSchema( - plannerConfig, + return QueryFrameworkUtils.createMockRootSchemaProvider( viewManager, authorizerMapper, - druidSchema, - systemSchema, + druidSchemaProvider, + systemSchemaProvider, lookupSchema, - createOperatorTable + createOperatorTable, + plannerConfig ); - return rootSchema; } } ), @@ -826,7 +836,7 @@ public PlannerFixture( ) { this.viewManager = componentSupplier.createViewManager(); - final DruidSchemaCatalog rootSchema = QueryFrameworkUtils.createMockRootSchema( + final DruidSchemaCatalogProvider schemaProvider = QueryFrameworkUtils.createMockRootSchemaProvider( framework.injector, framework.conglomerate(), framework.walker(), @@ -839,7 +849,7 @@ public PlannerFixture( ); this.plannerFactory = new PlannerFactory( - rootSchema, + schemaProvider, framework.operatorTable(), framework.macroTable(), plannerConfig, diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestAuthorizer.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestAuthorizer.java index 18321c7269f4..e4b95c47591c 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestAuthorizer.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestAuthorizer.java @@ -38,6 +38,7 @@ public class TestAuthorizer *

  • resources of type DATASOURCE with names containing "restricted" for read include a policy restriction
  • *
  • superuser has full access
  • *
  • resources with names containing "forbidden" are denied
  • + *
  • resources of type DATASOURCE with names containing "readOnly" are denied for write actions
  • *
  • external resources are denied for read actions
  • *
  • resources of type DATASOURCE, VIEW, QUERY_CONTEXT, and EXTERNAL are allowed
  • *
  • if none of the roles above matches, deny access
  • @@ -49,6 +50,7 @@ public static Authorizer simple(String superuserName, Policy defaultPolicy) .defaultPolicyOnReadTable(defaultPolicy) .allowIfSuperuser(superuserName) .denyIfResourceNameHasKeyword("forbidden") + .denyWriteIfResourceNameHasKeyword("readOnly") .denyExternalRead() .allowIfResourceTypeIs(Set.of( ResourceType.DATASOURCE, @@ -105,6 +107,17 @@ public TestAuthorizer denyIfResourceNameHasKeyword(String keyword) return this; } + public TestAuthorizer denyWriteIfResourceNameHasKeyword(String keyword) + { + if (access.isPresent()) { + return this; + } + if (Action.WRITE.equals(action) && resource.getName().contains(keyword)) { + access = Optional.of(Access.DENIED); + } + return this; + } + public TestAuthorizer denyExternalRead() { if (access.isPresent()) { diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java index ddb13708fa81..d8752825bf64 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java @@ -850,6 +850,15 @@ public static SpecificSegmentsQuerySegmentWalker addDataSetsToWalker( .build(), index1 ).add( + DataSegment.builder() + .dataSource(CalciteTests.READ_ONLY_DATASOURCE) + .interval(index1.getDataInterval()) + .version("1") + .shardSpec(new LinearShardSpec(0)) + .size(0) + .build(), + index1 + ).add( DataSegment.builder() .dataSource(CalciteTests.FORBIDDEN_DATASOURCE) .interval(forbiddenIndex.getDataInterval()) diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDruidViewMacroFactory.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDruidViewMacroFactory.java index 1b0435070756..c6bea5d24174 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDruidViewMacroFactory.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDruidViewMacroFactory.java @@ -31,6 +31,11 @@ public DruidViewMacro create( String viewSql ) { - return new DruidViewMacro(plannerFactory, viewSql, CalciteTests.DRUID_SCHEMA_NAME); + return new DruidViewMacro( + plannerFactory, + viewSql, + CalciteTests.DRUID_SCHEMA_NAME, + CalciteTests.TEST_AUTHENTICATOR_ESCALATOR + ); } } diff --git a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java index 182460993a74..8aa72e7b67a6 100644 --- a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java +++ b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java @@ -82,7 +82,6 @@ import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.AuthorizationResult; -import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.server.security.ResourceAction; import org.apache.druid.sql.DirectStatement; import org.apache.druid.sql.HttpStatement; @@ -102,7 +101,7 @@ import org.apache.druid.sql.calcite.planner.PlannerFactory; import org.apache.druid.sql.calcite.planner.PlannerResult; import org.apache.druid.sql.calcite.run.NativeSqlEngine; -import org.apache.druid.sql.calcite.schema.DruidSchemaCatalog; +import org.apache.druid.sql.calcite.schema.DruidSchemaCatalogProvider; import org.apache.druid.sql.calcite.util.CalciteTestBase; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.hook.DruidHookDispatcher; @@ -236,7 +235,7 @@ public void setUp() throws Exception executorService = MoreExecutors.listeningDecorator(Execs.multiThreaded(8, "test_sql_resource_%s")); final PlannerConfig plannerConfig = PlannerConfig.builder().build(); - final DruidSchemaCatalog rootSchema = CalciteTests.createMockRootSchema( + final DruidSchemaCatalogProvider schemaProvider = CalciteTests.createMockRootSchemaProvider( conglomerate, walker, plannerConfig, @@ -250,7 +249,7 @@ public void setUp() throws Exception testRequestLogger = new TestRequestLogger(); final PlannerFactory plannerFactory = new PlannerFactory( - rootSchema, + schemaProvider, operatorTable, macroTable, plannerConfig, @@ -354,14 +353,18 @@ public void tearDown() throws Exception } @Test - public void testUnauthorized() + public void testUnauthorized() throws Exception { - ForbiddenException e = Assertions.assertThrows(ForbiddenException.class, () -> { - postForAsyncResponse(createSimpleQueryWithId("id", "select count(*) from forbiddenDatasource"), request()); - }); - Assertions.assertEquals("Unauthorized", e.getMessage()); + // Unauthorized tables are validation errors ("not found") because DruidSchemaProvider filters them out. + ErrorResponse errorResponse = + postSyncForException("select count(*) from forbiddenDatasource", Status.BAD_REQUEST.getStatusCode()); + + validateInvalidSqlError( + errorResponse, + "Object 'forbiddenDatasource' not found" + ); + Assertions.assertEquals(1, testRequestLogger.getSqlQueryLogs().size()); - Assertions.assertTrue(lifecycleManager.getAll("id").isEmpty()); } @Test From a2207ab107188041dc95ca5fc36676026c2a5b3e Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Wed, 19 Aug 2026 15:22:27 -0700 Subject: [PATCH 2/3] Fix issues from testing. --- .../testing/embedded/query/JdbcQueryTest.java | 4 ++-- .../org/apache/druid/grpc/BasicAuthTest.java | 10 +++++++-- .../controller/http/DartSqlResourceTest.java | 22 ++++++++++++++----- .../druid/sql/calcite/CalciteQueryTest.java | 2 ++ .../sql/calcite/CalciteReplaceDmlTest.java | 2 -- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/query/JdbcQueryTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/query/JdbcQueryTest.java index 9fc52f915bfe..f559d44220b2 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/query/JdbcQueryTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/query/JdbcQueryTest.java @@ -99,8 +99,8 @@ public void testJdbcMetadata() schemas.add(schema); } LOG.info("'druid' catalog schemas %s", schemas); - // maybe more schemas than this, but at least should have these - Assertions.assertTrue(schemas.containsAll(ImmutableList.of("INFORMATION_SCHEMA", "druid", "lookup", "sys"))); + // Minimum set of schemas. + Assertions.assertTrue(schemas.containsAll(ImmutableList.of("INFORMATION_SCHEMA", "druid", "sys"))); Set druidTables = new HashSet<>(); ResultSet tablesMetadata = metadata.getTables("druid", "druid", null, null); diff --git a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java index c5806102995c..5b4cd4f69c1c 100644 --- a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java +++ b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java @@ -40,6 +40,8 @@ import org.apache.druid.sql.calcite.BaseCalciteQueryTest; import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.calcite.util.SqlTestFramework; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -234,8 +236,12 @@ public void testUnauthorized() .build(); try (TestClient client = new TestClient(TestClient.DEFAULT_HOST, "regular", "pwd")) { - StatusRuntimeException e = assertThrows(StatusRuntimeException.class, () -> client.getQueryClient().submitQuery(request)); - assertEquals(Status.PERMISSION_DENIED, e.getStatus()); + QueryResponse response = client.getQueryClient().submitQuery(request); + assertEquals(QueryStatus.RUNTIME_ERROR, response.getStatus()); + MatcherAssert.assertThat( + response.getErrorMessage(), + CoreMatchers.startsWith("Object 'forbiddenDatasource' not found") + ); } } } diff --git a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java index 6fce3cf71cb6..7bf57e54d132 100644 --- a/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java +++ b/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java @@ -72,7 +72,6 @@ import org.apache.druid.server.mocks.MockHttpServletResponse; import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthenticationResult; -import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.sql.SqlLifecycleManager; import org.apache.druid.sql.SqlToolbox; import org.apache.druid.sql.calcite.planner.CalciteRulesManager; @@ -560,7 +559,7 @@ public void test_doPost_informationSchema() Assertions.assertNull(sqlResource.doPost(sqlQuery, httpServletRequest)); Assertions.assertEquals(Response.Status.OK.getStatusCode(), asyncResponse.getStatus()); Assertions.assertEquals( - "[[\"INFORMATION_SCHEMA\"],[\"druid\"],[\"lookup\"],[\"sys\"],[\"view\"]]\n", + "[[\"INFORMATION_SCHEMA\"],[\"druid\"],[\"lookup\"],[\"sys\"]]\n", StringUtils.fromUtf8(asyncResponse.baos.toByteArray()) ); } @@ -596,7 +595,7 @@ public void test_doPost_sysTableJoinedToDatasource() } @Test - public void test_doPost_regularUser_forbidden() + public void test_doPost_regularUser_unauthorizedTable() { final MockAsyncContext asyncContext = new MockAsyncContext(); final MockHttpServletResponse asyncResponse = new MockHttpServletResponse(); @@ -617,9 +616,20 @@ public void test_doPost_regularUser_forbidden() Collections.emptyList() ); - Assertions.assertThrows( - ForbiddenException.class, - () -> sqlResource.doPost(sqlQuery, httpServletRequest) + // 400 Bad Request: the table is not visible to this user, so it cannot be resolved. + final Response response = sqlResource.doPost(sqlQuery, httpServletRequest); + Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + final Map e = objectMapper.convertValue( + response.getEntity(), + JacksonUtils.TYPE_REFERENCE_MAP_STRING_OBJECT + ); + + Assertions.assertEquals("invalidInput", e.get("errorCode")); + Assertions.assertEquals("INVALID_INPUT", e.get("category")); + assertThat( + (String) e.get("errorMessage"), + CoreMatchers.startsWith("Object 'forbiddenDatasource' not found") ); } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java index 294c148a9b5d..0010d7b0ad54 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java @@ -281,6 +281,7 @@ WHERE TABLE_TYPE IN ('SYSTEM_TABLE', 'TABLE', 'VIEW') @Test public void testInformationSchemaTables_superUser() { + msqIncompatible(); testQuery( PLANNER_CONFIG_DEFAULT, """ @@ -390,6 +391,7 @@ public void testInformationSchemaColumnsOnForbiddenTable_regularUser_noAuthorize @Test public void testInformationSchemaColumnsOnForbiddenTable_superUser() { + msqIncompatible(); testQuery( PLANNER_CONFIG_DEFAULT, """ diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java index a429dc4d4bcc..5e71878a4269 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteReplaceDmlTest.java @@ -47,8 +47,6 @@ import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.util.CalciteTests; import org.junit.jupiter.api.Assertions; -import org.hamcrest.CoreMatchers; -import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.jupiter.api.Test; import java.io.IOException; From 3b76e29c596a0936ca7fad5ef4b2415efd174a03 Mon Sep 17 00:00:00 2001 From: Gian Merlino Date: Wed, 19 Aug 2026 16:18:49 -0700 Subject: [PATCH 3/3] Fixes related to tests. --- .../org/apache/druid/grpc/BasicAuthTest.java | 4 ++-- .../SqlMSQStatementResourcePostTest.java | 20 +++++++++++++++++-- .../sql/avatica/DruidAvaticaHandlerTest.java | 12 +++++------ .../DruidPlannerResourceAnalyzeTest.java | 9 ++++----- .../sql/calcite/util/TestDataBuilder.java | 6 ++---- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java index 5b4cd4f69c1c..d37531353ce8 100644 --- a/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java +++ b/extensions-contrib/grpc-query/src/test/java/org/apache/druid/grpc/BasicAuthTest.java @@ -41,7 +41,6 @@ import org.apache.druid.sql.calcite.util.CalciteTests; import org.apache.druid.sql.calcite.util.SqlTestFramework; import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -50,6 +49,7 @@ import java.io.IOException; import java.util.Map; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -238,7 +238,7 @@ public void testUnauthorized() try (TestClient client = new TestClient(TestClient.DEFAULT_HOST, "regular", "pwd")) { QueryResponse response = client.getQueryClient().submitQuery(request); assertEquals(QueryStatus.RUNTIME_ERROR, response.getStatus()); - MatcherAssert.assertThat( + assertThat( response.getErrorMessage(), CoreMatchers.startsWith("Object 'forbiddenDatasource' not found") ); diff --git a/multi-stage-query/src/test/java/org/apache/druid/msq/sql/resources/SqlMSQStatementResourcePostTest.java b/multi-stage-query/src/test/java/org/apache/druid/msq/sql/resources/SqlMSQStatementResourcePostTest.java index bfc6fd3ef8a6..236dd17f51ba 100644 --- a/multi-stage-query/src/test/java/org/apache/druid/msq/sql/resources/SqlMSQStatementResourcePostTest.java +++ b/multi-stage-query/src/test/java/org/apache/druid/msq/sql/resources/SqlMSQStatementResourcePostTest.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableMap; import org.apache.calcite.sql.type.SqlTypeName; import org.apache.druid.error.DruidException; +import org.apache.druid.error.ErrorResponse; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.java.util.common.guava.Sequences; import org.apache.druid.java.util.common.guava.Yielders; @@ -49,6 +50,7 @@ import org.apache.druid.sql.http.ResultFormat; import org.apache.druid.sql.http.SqlQuery; import org.apache.druid.storage.NilStorageConnector; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -64,6 +66,8 @@ import java.util.List; import java.util.Map; +import static org.hamcrest.MatcherAssert.assertThat; + public class SqlMSQStatementResourcePostTest extends MSQTestBase { private SqlStatementResource resource; @@ -312,7 +316,8 @@ public void testExplain() throws IOException @Test public void forbiddenTest() { - Assert.assertEquals(Response.Status.FORBIDDEN.getStatusCode(), resource.doPost( + // The datasource is not visible to this user, so it cannot be resolved. + final Response response = resource.doPost( new SqlQuery( StringUtils.format("select * from %s", CalciteTests.FORBIDDEN_DATASOURCE), null, @@ -323,7 +328,18 @@ public void forbiddenTest() null ), SqlStatementResourceTest.makeOkRequest() - ).getStatus()); + ); + + Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + final DruidException e = ((ErrorResponse) response.getEntity()).getUnderlyingException(); + Assert.assertEquals(DruidException.Category.INVALID_INPUT, e.getCategory()); + assertThat( + e.getMessage(), + CoreMatchers.startsWith( + StringUtils.format("Object '%s' not found", CalciteTests.FORBIDDEN_DATASOURCE) + ) + ); } @Test diff --git a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java index 9ea180c6f469..b2b21e1ea898 100644 --- a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java +++ b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java @@ -95,10 +95,8 @@ import org.apache.druid.sql.hook.DruidHookDispatcher; import org.eclipse.jetty.server.Server; import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -139,6 +137,8 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +import static org.hamcrest.MatcherAssert.assertThat; + /** * Tests the Avatica-based JDBC implementation using JSON serialization. See * {@link DruidAvaticaProtobufHandlerTest} for a subclass which runs @@ -1729,11 +1729,9 @@ public void testUnauthorizedTable() } } ); - MatcherAssert.assertThat( - e, - ThrowableMessageMatcher.hasMessage( - CoreMatchers.containsString("Object '" + CalciteTests.FORBIDDEN_DATASOURCE + "' not found") - ) + assertThat( + e.getMessage(), + CoreMatchers.containsString("Object '" + CalciteTests.FORBIDDEN_DATASOURCE + "' not found") ); } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/DruidPlannerResourceAnalyzeTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/DruidPlannerResourceAnalyzeTest.java index 5f00c9745c35..3d9887f9b0fe 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/DruidPlannerResourceAnalyzeTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/DruidPlannerResourceAnalyzeTest.java @@ -30,8 +30,6 @@ import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.util.CalciteTests; import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; -import org.junit.internal.matchers.ThrowableMessageMatcher; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -39,6 +37,7 @@ import java.util.List; import java.util.Map; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; public class DruidPlannerResourceAnalyzeTest extends BaseCalciteQueryTest @@ -403,9 +402,9 @@ public void testTableAppendUnauthorizedTable() .run() ); - MatcherAssert.assertThat( - e, - ThrowableMessageMatcher.hasMessage(CoreMatchers.containsString("Table [forbiddenDatasource] not found")) + assertThat( + e.getMessage(), + CoreMatchers.containsString("Table [forbiddenDatasource] not found") ); // The superuser can see it. diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java index d8752825bf64..41375dbeb470 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java @@ -92,6 +92,7 @@ import org.apache.druid.server.SpecificSegmentsQuerySegmentWalker; import org.apache.druid.sql.calcite.util.datasets.TestDataSet; import org.apache.druid.timeline.DataSegment; +import org.apache.druid.timeline.SegmentId; import org.apache.druid.timeline.partition.LinearShardSpec; import org.apache.druid.timeline.partition.NumberedShardSpec; import org.joda.time.DateTime; @@ -850,10 +851,7 @@ public static SpecificSegmentsQuerySegmentWalker addDataSetsToWalker( .build(), index1 ).add( - DataSegment.builder() - .dataSource(CalciteTests.READ_ONLY_DATASOURCE) - .interval(index1.getDataInterval()) - .version("1") + DataSegment.builder(SegmentId.of(CalciteTests.READ_ONLY_DATASOURCE, index1.getDataInterval(), "1", 0)) .shardSpec(new LinearShardSpec(0)) .size(0) .build(),