From 1fe6c3f6ee2e1c34afe4114ea5e872d34e1a112a Mon Sep 17 00:00:00 2001
From: shfshihuafeng
Date: Wed, 15 Jul 2026 11:03:13 +0800
Subject: [PATCH] DRILL-8548: Integrate Apache Ranger authorization for Drill
---
auth/drill-ranger-plugin-shim/pom.xml | 99 ++++
.../ranger/DrillRangerPluginClassLoader.java | 119 ++++
.../ranger/RangerAccessAuthorizer.java | 242 +++++++++
.../ranger/RangerAccessAuthorizerFactory.java | 60 ++
....exec.security.spi.AccessAuthorizerFactory | 15 +
.../ranger/RangerAccessAuthorizerTest.java | 211 ++++++++
.../drill/authorizer/DrillAccessControl.java | 116 ++++
auth/drill-ranger-plugin/pom.xml | 298 ++++++++++
.../drill/authorizer/DrillAccessControl.java | 279 ++++++++++
.../drill/authorizer/DrillAuthorizer.java | 178 ++++++
.../authorizer/RangerBaseAuthorizer.java | 118 ++++
.../drill/authorizer/RangerDrillPlugin.java | 32 ++
.../drill/resource/DrillAccessResource.java | 87 +++
.../drill/resource/DrillAccessType.java | 21 +
.../resource/DrillRangerAccessRequest.java | 145 +++++
.../authorizer/DrillAccessControlTest.java | 318 +++++++++++
.../drill/authorizer/DrillAuthorizerTest.java | 196 +++++++
.../authorizer/RangerBaseAuthorizerTest.java | 134 +++++
.../resource/DrillAccessResourceTest.java | 104 ++++
.../DrillRangerAccessRequestTest.java | 156 ++++++
auth/pom.xml | 79 +++
distribution/pom.xml | 234 ++++++++
distribution/src/assemble/component.xml | 100 +++-
.../src/main/resources/drill-config.sh | 5 +
distribution/src/main/resources/logback.xml | 8 +
.../resources/ranger/ranger-drill-audit.xml | 75 +++
.../ranger/ranger-drill-security.xml | 57 ++
docs/dev/DevDocs.md | 7 +
docs/dev/RangerAuthorization.md | 383 +++++++++++++
exec/java-exec/pom.xml | 11 +
.../org/apache/drill/exec/ExecConstants.java | 4 +
.../sql/conversion/ColumnAccessChecker.java | 511 ++++++++++++++++++
.../conversion/DrillCalciteCatalogReader.java | 52 +-
.../planner/sql/conversion/SqlConverter.java | 14 +
.../sql/handlers/CreateTableHandler.java | 11 +
.../sql/handlers/DropTableHandler.java | 10 +
.../planner/sql/handlers/ViewHandler.java | 18 +
.../security/AccessAuthorizerManager.java | 189 +++++++
.../security/AllowAllAccessAuthorizer.java | 44 ++
.../drill/exec/security/DdlAccessChecker.java | 86 +++
.../exec/security/TableAccessResource.java | 130 +++++
.../apache/drill/exec/server/Drillbit.java | 10 +
.../ColumnAccessCheckerCorrelatedTest.java | 479 ++++++++++++++++
.../security/AccessAuthorizerManagerTest.java | 253 +++++++++
.../AllowAllAccessAuthorizerTest.java | 63 +++
.../exec/security/DdlAccessCheckerTest.java | 150 +++++
.../security/TableAccessResourceTest.java | 140 +++++
.../exec/security/TestAccessAuthorizer.java | 161 ++++++
.../security/TestAccessAuthorizerFactory.java | 46 ++
.../exec/sql/TestWithClauseRangerAuthz.java | 170 ++++++
....exec.security.spi.AccessAuthorizerFactory | 15 +
.../src/test/resources/core-site.xml | 8 +
exec/pom.xml | 1 +
exec/security-spi/pom.xml | 42 ++
.../exec/security/spi/AccessAuthorizer.java | 96 ++++
.../security/spi/AccessAuthorizerFactory.java | 54 ++
.../drill/exec/security/spi/AccessType.java | 41 ++
.../drill/exec/security/spi/UserIdentity.java | 117 ++++
pom.xml | 53 ++
59 files changed, 6849 insertions(+), 6 deletions(-)
create mode 100644 auth/drill-ranger-plugin-shim/pom.xml
create mode 100644 auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/DrillRangerPluginClassLoader.java
create mode 100644 auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizer.java
create mode 100644 auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizerFactory.java
create mode 100644 auth/drill-ranger-plugin-shim/src/main/resources/META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory
create mode 100644 auth/drill-ranger-plugin-shim/src/test/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizerTest.java
create mode 100644 auth/drill-ranger-plugin-shim/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControl.java
create mode 100644 auth/drill-ranger-plugin/pom.xml
create mode 100644 auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControl.java
create mode 100644 auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizer.java
create mode 100644 auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizer.java
create mode 100644 auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerDrillPlugin.java
create mode 100644 auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessResource.java
create mode 100644 auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessType.java
create mode 100644 auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequest.java
create mode 100644 auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControlTest.java
create mode 100644 auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizerTest.java
create mode 100644 auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizerTest.java
create mode 100644 auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillAccessResourceTest.java
create mode 100644 auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequestTest.java
create mode 100644 auth/pom.xml
create mode 100644 distribution/src/main/resources/ranger/ranger-drill-audit.xml
create mode 100644 distribution/src/main/resources/ranger/ranger-drill-security.xml
create mode 100644 docs/dev/RangerAuthorization.md
create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/ColumnAccessChecker.java
create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/security/AccessAuthorizerManager.java
create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/security/AllowAllAccessAuthorizer.java
create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/security/DdlAccessChecker.java
create mode 100644 exec/java-exec/src/main/java/org/apache/drill/exec/security/TableAccessResource.java
create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/planner/sql/conversion/ColumnAccessCheckerCorrelatedTest.java
create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/security/AccessAuthorizerManagerTest.java
create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/security/AllowAllAccessAuthorizerTest.java
create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/security/DdlAccessCheckerTest.java
create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/security/TableAccessResourceTest.java
create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/security/TestAccessAuthorizer.java
create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/security/TestAccessAuthorizerFactory.java
create mode 100644 exec/java-exec/src/test/java/org/apache/drill/exec/sql/TestWithClauseRangerAuthz.java
create mode 100644 exec/java-exec/src/test/resources/META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory
create mode 100644 exec/security-spi/pom.xml
create mode 100644 exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessAuthorizer.java
create mode 100644 exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessAuthorizerFactory.java
create mode 100644 exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessType.java
create mode 100644 exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/UserIdentity.java
diff --git a/auth/drill-ranger-plugin-shim/pom.xml b/auth/drill-ranger-plugin-shim/pom.xml
new file mode 100644
index 00000000000..52e3c20cd92
--- /dev/null
+++ b/auth/drill-ranger-plugin-shim/pom.xml
@@ -0,0 +1,99 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.drill
+ auth-parent
+ 1.23.0-SNAPSHOT
+
+
+ drill-ranger-plugin-shim
+ Drill : Ranger Drill Plugin Shim
+
+ Thin shim between the Drillbit and the drill-ranger-plugin authorization
+ module. Lives on the Drillbit's main classpath and implements the
+ org.apache.drill.exec.security.spi.AccessAuthorizer SPI; delegates all
+ calls to DrillAccessControl (in drill-ranger-plugin) via reflection
+ through an isolated DrillRangerPluginClassLoader.
+
+ Mirrors Presto's ranger-presto-plugin-shim in the Ranger repository:
+ - drill-ranger-plugin-shim : Drillbit main classpath (this module)
+ - drill-ranger-plugin : RangerPluginClassLoader isolated directory
+ (jars/ranger-drill-plugin-impl/)
+
+ The shim deliberately has NO compile-time dependency on
+ drill-ranger-plugin: DrillAccessControl is referenced only via a string
+ constant and reflection, so the two jars are coupled solely at runtime
+ through the plugin classloader.
+
+ The Drillbit discovers this module's RangerAccessAuthorizerFactory via
+ META-INF/services (ServiceLoader) registered in this jar.
+
+
+
+
+
+ org.apache.drill
+ drill-security-spi
+ ${project.version}
+ provided
+
+
+
+ org.apache.ranger
+ ranger-plugin-classloader
+ ${ranger.version}
+ provided
+
+
+ org.slf4j
+ slf4j-api
+ provided
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+
diff --git a/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/DrillRangerPluginClassLoader.java b/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/DrillRangerPluginClassLoader.java
new file mode 100644
index 00000000000..1221296cd73
--- /dev/null
+++ b/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/DrillRangerPluginClassLoader.java
@@ -0,0 +1,119 @@
+/*
+ * 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.drill.exec.security.ranger;
+
+import org.apache.ranger.plugin.classloader.RangerPluginClassLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * A {@link RangerPluginClassLoader} subclass that blocks the Jersey 3.1.9
+ * MultiPart SPI from leaking into the Jersey 2.35 Ranger client via the
+ * parent (Drillbit) classpath.
+ */
+public final class DrillRangerPluginClassLoader extends RangerPluginClassLoader {
+
+ private static final Logger logger = LoggerFactory.getLogger(DrillRangerPluginClassLoader.class);
+
+ /**
+ * SPI resource name that Jersey's {@code ServiceFinder} scans to locate
+ * auto-discoverable providers. Jersey 2.35 and 3.1.9 share this file
+ * name (the SPI contract is package-private and unchanged across the
+ * two versions).
+ */
+ private static final String AUTODISCOVERABLE_SPI =
+ "META-INF/services/org.glassfish.jersey.internal.spi.AutoDiscoverable";
+
+ /**
+ * FQN that exists only in Jersey 3.x. Its presence in an
+ * {@code AutoDiscoverable} SPI file is a reliable marker that the file
+ * comes from a Jersey 3.1.9 jar on the Drillbit classpath and must be
+ * hidden from the 2.35 {@code ServiceFinder}.
+ */
+ private static final String JERSEY3_MULTIPART_MARKER =
+ "org.glassfish.jersey.media.multipart.MultiPartFeatureAutodiscoverable";
+
+ public DrillRangerPluginClassLoader(String pluginType, Class> pluginClass) throws Exception {
+ super(pluginType, pluginClass);
+ logger.info("DrillRangerPluginClassLoader initialized for plugin type: {}", pluginType);
+ }
+
+ /**
+ * Returns merged child+component resources, with the Jersey 3.1.9
+ * MultiPart {@code AutoDiscoverable} SPI entry removed when present.
+ *
+ * Non-SPI resources are returned unchanged so that Ranger's own
+ * resource lookups (configuration files, native libraries, etc.) are
+ * not affected.
+ */
+ @Override
+ public Enumeration findResources(String name) {
+ // Base RangerPluginClassLoader.findResources does not declare IOException,
+ // so super.findResources cannot throw it either; no try/catch needed.
+ Enumeration merged = super.findResources(name);
+ if (!AUTODISCOVERABLE_SPI.equals(name)) {
+ return merged;
+ }
+ List kept = new ArrayList<>();
+ while (merged.hasMoreElements()) {
+ URL url = merged.nextElement();
+ if (!declaresJersey3Multipart(url)) {
+ kept.add(url);
+ } else {
+ logger.debug("Filtered Jersey 3.1.9 MultiPart AutoDiscoverable SPI entry: {}", url);
+ }
+ }
+ return Collections.enumeration(kept);
+ }
+
+ /**
+ * Returns {@code true} if the given SPI resource URL declares the
+ * Jersey 3.x {@code MultiPartFeatureAutodiscoverable} FQN.
+ *
+ * If the URL cannot be read, conservatively returns {@code false}
+ * (keep the entry) to avoid accidentally dropping a legitimate SPI
+ * file that might be unreadable due to transient I/O conditions.
+ */
+ private boolean declaresJersey3Multipart(URL url) {
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ // SPI files use FQNs (optionally with comment/whitespace); a plain
+ // contains() check is sufficient and avoids false negatives from
+ // edge-case formatting (trailing spaces, trailing comments).
+ if (line.contains(JERSEY3_MULTIPART_MARKER)) {
+ return true;
+ }
+ }
+ } catch (IOException e) {
+ logger.warn("Could not read SPI entry {} to inspect content; keeping it", url, e);
+ return false;
+ }
+ return false;
+ }
+}
diff --git a/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizer.java b/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizer.java
new file mode 100644
index 00000000000..ee5b8ffe497
--- /dev/null
+++ b/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizer.java
@@ -0,0 +1,242 @@
+/*
+ * 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.drill.exec.security.ranger;
+
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.apache.ranger.plugin.classloader.RangerPluginClassLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+
+/**
+ * {@link AccessAuthorizer} SPI implementation backed by Ranger.
+ *
+ * This class is a thin shim on the Drillbit's main classpath. It creates
+ * the delegate {@code DrillAccessControl} (in the {@code drill-ranger-plugin}
+ * module) once via reflection, casts it to {@link AccessAuthorizer} and then
+ * invokes all checks as direct virtual calls through the SPI interface.
+ * The cast is safe because the plugin classloader is child-first but falls
+ * back to the Drillbit classloader for SPI types: both sides resolve
+ * {@code AccessAuthorizer}/{@code UserIdentity} to the same Class objects
+ * from drill-security-spi.jar on the main classpath.
+ *
+ * The classloader isolation is required because the Ranger plugin ships
+ * Jersey 2.35 ({@code org.glassfish.jersey.*} + {@code javax.ws.rs.*}) for
+ * {@code RangerAdminJersey2RESTClient}, while Drill's own REST server uses
+ * Jersey 3.1.9 ({@code org.glassfish.jersey.*} + {@code jakarta.ws.rs.*}).
+ * Both Jersey versions share the {@code org.glassfish.jersey.*} implementation
+ * package name but bind to incompatible API namespaces, so they cannot coexist
+ * in a single classloader. The {@link RangerPluginClassLoader} uses a
+ * child-first strategy to load plugin classes from its private URL list,
+ * falling back to the Drillbit classloader for shared types (Hadoop, SLF4J,
+ * the Drill security SPI, etc.).
+ *
+ * Drill uses the subclass {@link DrillRangerPluginClassLoader} instead of
+ * the base {@link RangerPluginClassLoader} to filter out the Jersey 3.1.9
+ * {@code MultiPartFeatureAutodiscoverable} SPI entry that the base
+ * {@code findResources} merge would otherwise leak from the Drillbit
+ * classpath into Jersey 2.35's {@code ServiceFinder}. See
+ * {@link DrillRangerPluginClassLoader} for the root-cause analysis.
+ *
+ * Every delegated call is wrapped in
+ * {@code activateClassLoader()/deactivateClassLoader()} — the same
+ * per-call TCCL contract used by Presto's {@code RangerSystemAccessControl}
+ * and the other Ranger plugin shims.
+ */
+public class RangerAccessAuthorizer implements AccessAuthorizer {
+
+ private static final Logger logger = LoggerFactory.getLogger(RangerAccessAuthorizer.class);
+
+ private static final String RANGER_PLUGIN_TYPE = "drill";
+ private static final String DRILL_ACCESS_CONTROL_CLASS =
+ "org.apache.ranger.authorization.drill.authorizer.DrillAccessControl";
+
+ private final RangerPluginClassLoader pluginClassLoader;
+
+ // Delegate created once in the constructor; all checks are direct virtual
+ // calls through the SPI interface (no per-call reflection). Final: the
+ // constructor either assigns it or throws (fail-closed), so an instance
+ // that exists always has a working delegate.
+ private final AccessAuthorizer delegate;
+
+ /**
+ * Creates the shim and completes all initialization (mirrors Presto's
+ * RangerSystemAccessControl constructor): acquires the
+ * {@link RangerPluginClassLoader}, reflectively instantiates
+ * {@code DrillAccessControl(serviceName)} from the isolated
+ * {@code ranger-drill-plugin-impl/} directory inside an
+ * activated-classloader block, and casts it to {@link AccessAuthorizer}.
+ *
+ * @param serviceName the Ranger service instance name
+ * @throws RuntimeException if initialization fails (fail-closed)
+ */
+ public RangerAccessAuthorizer(String serviceName) {
+ this(createProductionClassLoader(), serviceName);
+ }
+
+ /**
+ * Acquires the singleton production {@link DrillRangerPluginClassLoader}.
+ * Initialization failures (including {@link ExceptionInInitializerError}
+ * from the lazy holder) are wrapped in {@link RuntimeException} so the
+ * factory always surfaces a consistent fail-closed exception type.
+ */
+ private static RangerPluginClassLoader createProductionClassLoader() {
+ try {
+ RangerPluginClassLoader cl = DrillRangerPluginClassLoaderHolder.INSTANCE;
+ logger.info("DrillRangerPluginClassLoader initialized for plugin type: {}", RANGER_PLUGIN_TYPE);
+ return cl;
+ } catch (Throwable t) {
+ logger.error("Failed to create DrillRangerPluginClassLoader", t);
+ throw new RuntimeException(
+ "Failed to create DrillRangerPluginClassLoader: " + t.getMessage(), t);
+ }
+ }
+
+ /**
+ * Package-private constructor for unit tests. Allows injecting a mock
+ * {@link RangerPluginClassLoader} directly, bypassing
+ * {@link RangerPluginClassLoader#getInstance} — which Mockito cannot mock
+ * because it is a {@link ClassLoader} subclass (mocking class-loader
+ * statics risks infinite recursion).
+ *
+ * When a non-null classloader is supplied, the delegate is loaded and
+ * instantiated through the injected classloader instead of the production
+ * holder; the mock typically delegates {@code loadClass} to the test
+ * classloader so the test stub class (same FQCN) is instantiated.
+ */
+ RangerAccessAuthorizer(RangerPluginClassLoader pluginClassLoader, String serviceName) {
+ try {
+ this.pluginClassLoader = pluginClassLoader;
+ activateClassLoader();
+ try {
+ Class> clazz = pluginClassLoader.loadClass(DRILL_ACCESS_CONTROL_CLASS);
+ delegate = (AccessAuthorizer) clazz.getConstructor(String.class).newInstance(serviceName);
+ } finally {
+ deactivateClassLoader();
+ }
+ } catch (Exception e) {
+ logger.error("Failed to initialize RangerAccessAuthorizer via PluginClassLoader", e);
+ throw new RuntimeException("Failed to initialize RangerAccessAuthorizer: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Checks table-level access permission by delegating directly to the
+ * {@code DrillAccessControl} instance through the SPI interface.
+ * Fail-closed (returns {@code false}) on invocation error.
+ *
+ * @param user the querying user identity
+ * @param dataSource the data source name (StoragePlugin name, e.g. "dfs")
+ * @param schema the schema path (e.g. "dfs.tmp")
+ * @param table the table name
+ * @param accessType the access type (e.g. {@code AccessType.SELECT})
+ * @return {@code true} if access is allowed
+ */
+ @Override
+ public boolean checkTableAccess(UserIdentity user, String dataSource, String schema,
+ String table, AccessType accessType) {
+ activateClassLoader();
+ try {
+ return delegate.checkTableAccess(user, dataSource, schema, table, accessType);
+ } catch (Exception e) {
+ logger.error("Failed to invoke DrillAccessControl.checkTableAccess()", e);
+ return false; // fail-closed on error
+ } finally {
+ deactivateClassLoader();
+ }
+ }
+
+ /**
+ * Checks column-level access permission by delegating directly to the
+ * {@code DrillAccessControl} instance through the SPI interface.
+ * Fail-closed (returns {@code false}) on invocation error.
+ *
+ * @param user the querying user identity
+ * @param dataSource the data source name (StoragePlugin name, e.g. "dfs")
+ * @param schema the schema path (e.g. "dfs.tmp")
+ * @param table the table name
+ * @param columns the set of column names being accessed
+ * @param accessType the access type (e.g. {@code AccessType.SELECT})
+ * @return {@code true} if access is allowed for every column
+ */
+ @Override
+ public boolean checkColumnAccess(UserIdentity user, String dataSource, String schema,
+ String table, Set columns, AccessType accessType) {
+ activateClassLoader();
+ try {
+ return delegate.checkColumnAccess(user, dataSource, schema, table, columns, accessType);
+ } catch (Exception e) {
+ logger.error("Failed to invoke DrillAccessControl.checkColumnAccess()", e);
+ return false; // fail-closed on error
+ } finally {
+ deactivateClassLoader();
+ }
+ }
+
+ private void activateClassLoader() {
+ if (pluginClassLoader != null) {
+ pluginClassLoader.activate();
+ }
+ }
+
+ private void deactivateClassLoader() {
+ if (pluginClassLoader != null) {
+ pluginClassLoader.deactivate();
+ }
+ }
+
+ /**
+ * Releases the delegate's plugin resources (policy-refresh threads, policy
+ * caches) through the same classloader-activated contract used by the
+ * access checks. Forwarded by {@code AccessAuthorizerManager} when the
+ * Drillbit shuts down. Idempotent; failures are logged, never thrown, so a
+ * failing authorizer cannot abort Drill shutdown.
+ */
+ @Override
+ public void close() {
+ activateClassLoader();
+ try {
+ delegate.close();
+ } catch (Exception e) {
+ logger.warn("Failed to close DrillAccessControl", e);
+ } finally {
+ deactivateClassLoader();
+ }
+ }
+
+ /**
+ * Holder for the singleton {@link DrillRangerPluginClassLoader}. The
+ * base {@code RangerPluginClassLoader.getInstance()} cannot return our
+ * subclass, so Drill keeps its own single instance here. Initialized
+ * lazily on first class-loading of the enclosing authorizer.
+ */
+ private static final class DrillRangerPluginClassLoaderHolder {
+ static final RangerPluginClassLoader INSTANCE;
+
+ static {
+ try {
+ INSTANCE = new DrillRangerPluginClassLoader(
+ RANGER_PLUGIN_TYPE, RangerAccessAuthorizer.class);
+ } catch (Exception e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+ }
+}
diff --git a/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizerFactory.java b/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizerFactory.java
new file mode 100644
index 00000000000..64503f9009c
--- /dev/null
+++ b/auth/drill-ranger-plugin-shim/src/main/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizerFactory.java
@@ -0,0 +1,60 @@
+/*
+ * 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.drill.exec.security.ranger;
+
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessAuthorizerFactory;
+
+import java.util.Map;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * {@link AccessAuthorizerFactory} for the Ranger-backed authorizer.
+ * Registered via
+ * {@code META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory}
+ * and selected by {@code drill.exec.security.authorizer.name=ranger}. The
+ * config map carries the flattened {@code drill.exec.security.authorizer}
+ * subtree; Ranger-specific keys are parsed here (never in the engine):
+ *
+ *
+ * - {@code service.name} — Ranger service instance name
+ * (default {@code "drill"})
+ *
+ *
+ * {@link #createAuthorizer(Map)} returns a fully-initialized shim;
+ * initialization failures propagate as exceptions (fail-closed).
+ */
+public class RangerAccessAuthorizerFactory implements AccessAuthorizerFactory {
+
+ public static final String NAME = "ranger";
+
+ static final String CONFIG_SERVICE_NAME = "service.name";
+ static final String DEFAULT_SERVICE_NAME = "drill";
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public AccessAuthorizer createAuthorizer(Map config) {
+ requireNonNull(config, "config is null");
+ String serviceName = config.getOrDefault(CONFIG_SERVICE_NAME, DEFAULT_SERVICE_NAME);
+ return new RangerAccessAuthorizer(serviceName);
+ }
+}
diff --git a/auth/drill-ranger-plugin-shim/src/main/resources/META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory b/auth/drill-ranger-plugin-shim/src/main/resources/META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory
new file mode 100644
index 00000000000..625f2ee4c3e
--- /dev/null
+++ b/auth/drill-ranger-plugin-shim/src/main/resources/META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory
@@ -0,0 +1,15 @@
+# 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.
+org.apache.drill.exec.security.ranger.RangerAccessAuthorizerFactory
diff --git a/auth/drill-ranger-plugin-shim/src/test/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizerTest.java b/auth/drill-ranger-plugin-shim/src/test/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizerTest.java
new file mode 100644
index 00000000000..b2e23112f6b
--- /dev/null
+++ b/auth/drill-ranger-plugin-shim/src/test/java/org/apache/drill/exec/security/ranger/RangerAccessAuthorizerTest.java
@@ -0,0 +1,211 @@
+/*
+ * 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.drill.exec.security.ranger;
+
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.apache.ranger.authorization.drill.authorizer.DrillAccessControl;
+import org.apache.ranger.plugin.classloader.RangerPluginClassLoader;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link RangerAccessAuthorizer}.
+ *
+ * {@code RangerAccessAuthorizer} delegates to {@code DrillAccessControl}
+ * (in {@code drill-ranger-plugin}) through the {@link AccessAuthorizer}
+ * SPI interface. These tests verify the delegation by:
+ *
+ * - Injecting a mock {@link RangerPluginClassLoader} via the package-private
+ * constructor {@link RangerAccessAuthorizer#RangerAccessAuthorizer(RangerPluginClassLoader, String)}.
+ * This is necessary because Mockito refuses to mock static methods of
+ * {@link ClassLoader} subclasses (to avoid class-loading infinite loops),
+ * so the production classloader holder cannot be stubbed. The mock
+ * classloader's {@code loadClass(String)} delegates to the test
+ * classloader, so the reflective class lookup resolves to the test stub
+ * class that lives in the test source tree (same FQCN as the real
+ * plugin class).
+ * - Asserting against the stub's captured arguments and control knobs
+ * (see the stub {@link DrillAccessControl} for the available knobs).
+ * This exercises the real production path: reflective
+ * {@code getConstructor(String).newInstance(...)} + cast to
+ * {@link AccessAuthorizer} + direct virtual calls.
+ *
+ *
+ * Initialization happens in the constructor (mirroring Presto's
+ * RangerSystemAccessControl shim), so every test constructs the shim with a
+ * service name; the service name reaches the stub's constructor through the
+ * same reflective path used in production.
+ */
+public class RangerAccessAuthorizerTest {
+
+ private static final String USER = "alice";
+ private static final String DS = "mysql";
+ private static final String SCHEMA = "shf";
+ private static final String TABLE = "orders";
+
+ @Before
+ public void resetStub() {
+ DrillAccessControl.reset();
+ }
+
+ /**
+ * Builds a mock {@link RangerPluginClassLoader} whose {@code loadClass(String)}
+ * delegates to the test classloader. The reflective class lookup inside the
+ * shim constructor resolves the test stub class (DrillAccessControl) from
+ * the test classpath.
+ *
+ * {@code activate()} and {@code deactivate()} are no-ops on the mock
+ * (Mockito default behavior), which is exactly what we want — no TCCL
+ * switching during tests.
+ */
+ private RangerPluginClassLoader mockPluginClassLoader() {
+ RangerPluginClassLoader mockCl = mock(RangerPluginClassLoader.class);
+ ClassLoader testCl = RangerAccessAuthorizerTest.class.getClassLoader();
+ try {
+ when(mockCl.loadClass(anyString())).thenAnswer(inv -> {
+ String name = inv.getArgument(0);
+ return Class.forName(name, false, testCl);
+ });
+ } catch (ClassNotFoundException e) {
+ // mockCl.loadClass() on a Mockito mock never actually throws; this
+ // catch is only to satisfy the compiler's checked-exception analysis.
+ throw new RuntimeException(e);
+ }
+ return mockCl;
+ }
+
+ @Test
+ public void constructor_passesServiceNameToDrillAccessControl() {
+ RangerPluginClassLoader mockCl = mockPluginClassLoader();
+ new RangerAccessAuthorizer(mockCl, "mySvc");
+ assertEquals("mySvc", DrillAccessControl.lastServiceName);
+ }
+
+ @Test
+ public void constructor_throwsRuntimeException_whenClassLoadingFails() {
+ // Simulate loadClass() failure — the constructor wraps it in a
+ // RuntimeException (fail-closed) instead of leaking checked exceptions.
+ RangerPluginClassLoader mockCl = mock(RangerPluginClassLoader.class);
+ try {
+ when(mockCl.loadClass(anyString()))
+ .thenThrow(new ClassNotFoundException("class not found boom"));
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+
+ RuntimeException ex = assertThrows(
+ RuntimeException.class, () -> new RangerAccessAuthorizer(mockCl, "mySvc"));
+ assertTrue(ex.getMessage().contains("Failed to initialize RangerAccessAuthorizer"));
+ }
+
+ @Test
+ public void constructor_throwsRuntimeException_whenDelegateConstructionFails() {
+ // Simulate DrillAccessControl initialization failure (e.g. Ranger Admin
+ // unreachable): the shim must surface a RuntimeException (fail-closed).
+ DrillAccessControl.constructFails = true;
+ RangerPluginClassLoader mockCl = mockPluginClassLoader();
+
+ RuntimeException ex = assertThrows(
+ RuntimeException.class, () -> new RangerAccessAuthorizer(mockCl, "mySvc"));
+ assertTrue(ex.getMessage().contains("Failed to initialize RangerAccessAuthorizer"));
+ }
+
+ @Test
+ public void checkTableAccess_delegatesToDrillAccessControl() {
+ RangerPluginClassLoader mockCl = mockPluginClassLoader();
+ DrillAccessControl.result = true;
+ RangerAccessAuthorizer authorizer = new RangerAccessAuthorizer(mockCl, "mySvc");
+
+ assertTrue(authorizer.checkTableAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, AccessType.SELECT));
+
+ assertEquals(USER, DrillAccessControl.lastUser.getUser());
+ assertEquals(DS, DrillAccessControl.lastDataSource);
+ assertEquals(SCHEMA, DrillAccessControl.lastSchema);
+ assertEquals(TABLE, DrillAccessControl.lastTable);
+ assertEquals(AccessType.SELECT, DrillAccessControl.lastAccessType);
+ }
+
+ @Test
+ public void checkTableAccess_returnsFalse_whenInvocationThrows() {
+ RangerPluginClassLoader mockCl = mockPluginClassLoader();
+ DrillAccessControl.checkFailure = new RuntimeException("check boom");
+ RangerAccessAuthorizer authorizer = new RangerAccessAuthorizer(mockCl, "mySvc");
+
+ // fail-closed on error
+ assertFalse(authorizer.checkTableAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, AccessType.SELECT));
+ }
+
+ @Test
+ public void checkColumnAccess_delegatesToDrillAccessControl() {
+ RangerPluginClassLoader mockCl = mockPluginClassLoader();
+ DrillAccessControl.result = false;
+ RangerAccessAuthorizer authorizer = new RangerAccessAuthorizer(mockCl, "mySvc");
+
+ Set columns = new HashSet<>(Arrays.asList("id", "amount"));
+ assertFalse(authorizer.checkColumnAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, columns, AccessType.SELECT));
+
+ assertEquals(USER, DrillAccessControl.lastUser.getUser());
+ assertEquals(DS, DrillAccessControl.lastDataSource);
+ assertEquals(SCHEMA, DrillAccessControl.lastSchema);
+ assertEquals(TABLE, DrillAccessControl.lastTable);
+ assertEquals(columns, DrillAccessControl.lastColumns);
+ assertEquals(AccessType.SELECT, DrillAccessControl.lastAccessType);
+ }
+
+ @Test
+ public void checkColumnAccess_returnsFalse_whenInvocationThrows() {
+ RangerPluginClassLoader mockCl = mockPluginClassLoader();
+ DrillAccessControl.checkFailure = new RuntimeException("column check boom");
+ RangerAccessAuthorizer authorizer = new RangerAccessAuthorizer(mockCl, "mySvc");
+
+ Set columns = new HashSet<>(Arrays.asList("id"));
+ // fail-closed on error
+ assertFalse(authorizer.checkColumnAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, columns, AccessType.SELECT));
+ }
+
+ // ========================================================================
+ // Shutdown lifecycle
+ // ========================================================================
+
+ @Test
+ public void close_delegatesToDrillAccessControl() {
+ RangerPluginClassLoader mockCl = mockPluginClassLoader();
+ RangerAccessAuthorizer authorizer = new RangerAccessAuthorizer(mockCl, "mySvc");
+
+ authorizer.close();
+
+ assertEquals("close() must be forwarded to the delegate", 1,
+ DrillAccessControl.closeCount);
+ }
+}
diff --git a/auth/drill-ranger-plugin-shim/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControl.java b/auth/drill-ranger-plugin-shim/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControl.java
new file mode 100644
index 00000000000..7c9cc99ddac
--- /dev/null
+++ b/auth/drill-ranger-plugin-shim/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControl.java
@@ -0,0 +1,116 @@
+/*
+ * 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.ranger.authorization.drill.authorizer;
+
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+
+import java.util.Set;
+
+/**
+ * Test stub for the real {@code DrillAccessControl} class that lives in the
+ * {@code drill-ranger-plugin} module (loaded by the isolated
+ * {@code RangerPluginClassLoader} at runtime).
+ *
+ * Mirrors the real class's instance-based SPI contract: constructor
+ * {@code DrillAccessControl(String serviceName)} completes initialization,
+ * then all checks arrive as {@link AccessAuthorizer} virtual calls. The
+ * static fields below let tests control construction failure, check results
+ * and check failures, and inspect the arguments the shim forwarded.
+ */
+public class DrillAccessControl implements AccessAuthorizer {
+
+ /** Service name passed to the last constructor invocation. */
+ public static String lastServiceName;
+
+ /** When {@code true}, the constructor throws (simulates init failure). */
+ public static boolean constructFails;
+
+ /** Value returned by the check methods. */
+ public static boolean result = true;
+
+ /** When non-null, thrown by the check methods (fail-closed path). */
+ public static RuntimeException checkFailure;
+
+ /** Number of close() invocations (shutdown lifecycle). */
+ public static int closeCount;
+
+ // Arguments captured from the last check calls
+ public static UserIdentity lastUser;
+ public static String lastDataSource;
+ public static String lastSchema;
+ public static String lastTable;
+ public static AccessType lastAccessType;
+ public static Set lastColumns;
+
+ public DrillAccessControl(String serviceName) {
+ lastServiceName = serviceName;
+ if (constructFails) {
+ throw new RuntimeException("construct boom");
+ }
+ }
+
+ @Override
+ public boolean checkTableAccess(UserIdentity user, String dataSource, String schema,
+ String table, AccessType accessType) {
+ lastUser = user;
+ lastDataSource = dataSource;
+ lastSchema = schema;
+ lastTable = table;
+ lastAccessType = accessType;
+ if (checkFailure != null) {
+ throw checkFailure;
+ }
+ return result;
+ }
+
+ @Override
+ public boolean checkColumnAccess(UserIdentity user, String dataSource, String schema,
+ String table, Set columns, AccessType accessType) {
+ lastUser = user;
+ lastDataSource = dataSource;
+ lastSchema = schema;
+ lastTable = table;
+ lastColumns = columns;
+ lastAccessType = accessType;
+ if (checkFailure != null) {
+ throw checkFailure;
+ }
+ return result;
+ }
+
+ @Override
+ public void close() {
+ closeCount++;
+ }
+
+ /** Resets all control knobs and captured arguments. */
+ public static void reset() {
+ lastServiceName = null;
+ constructFails = false;
+ result = true;
+ checkFailure = null;
+ closeCount = 0;
+ lastUser = null;
+ lastDataSource = null;
+ lastSchema = null;
+ lastTable = null;
+ lastAccessType = null;
+ lastColumns = null;
+ }
+}
diff --git a/auth/drill-ranger-plugin/pom.xml b/auth/drill-ranger-plugin/pom.xml
new file mode 100644
index 00000000000..0d6c047811f
--- /dev/null
+++ b/auth/drill-ranger-plugin/pom.xml
@@ -0,0 +1,298 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.drill
+ auth-parent
+ 1.23.0-SNAPSHOT
+
+
+ drill-ranger-plugin
+ Drill : Ranger Drill Authorization Plugin
+
+ Apache Ranger authorization plugin for Apache Drill.
+ Loaded by the Drillbit at runtime; performs local in-memory policy
+ evaluation against policies pulled from Ranger Admin.
+
+
+
+
+ org.apache.drill
+ drill-security-spi
+ ${project.version}
+ provided
+
+
+ org.apache.ranger
+ ranger-plugins-common
+ ${ranger.version}
+
+
+ org.apache.ranger
+ ranger-plugins-cred
+
+
+ io.netty
+ *
+
+
+ org.slf4j
+ *
+
+
+ ch.qos.logback
+ *
+
+
+ org.apache.logging.log4j
+ *
+
+
+ log4j
+ log4j
+
+
+ commons-logging
+ commons-logging
+
+
+ org.apache.hadoop
+ *
+
+
+ com.fasterxml.jackson.core
+ *
+
+
+ com.fasterxml.jackson.dataformat
+ *
+
+
+ com.fasterxml.jackson.datatype
+ *
+
+
+ javax.servlet
+ *
+
+
+
+ com.sun.jersey
+ jersey-bundle
+
+
+ com.sun.jersey
+ jersey-json
+
+
+ org.ow2.asm
+ *
+
+
+
+
+
+ org.apache.ranger
+ ranger-audit-dest-log4j
+ ${ranger.version}
+
+
+ org.slf4j
+ *
+
+
+
+
+ org.apache.ranger
+ ranger-knox-plugin
+ ${ranger.version}
+
+
+ org.apache.knox
+ *
+
+
+ org.apache.ranger
+ ranger-knox-plugin-shim
+
+
+ org.apache.ranger
+ ranger-audit-dest-hdfs
+
+
+ org.apache.ranger
+ ranger-audit-dest-solr
+
+
+ org.apache.ranger
+ ranger-plugins-common
+
+
+ com.google.protobuf
+ protobuf-java
+
+
+ commons-collections
+ commons-collections
+
+
+ javax.servlet
+ *
+
+
+ org.apache.hadoop
+ hadoop-client-api
+
+
+ org.apache.hadoop
+ hadoop-client-runtime
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+ com.sun.jersey
+ *
+
+
+ javax.ws.rs
+ jsr311-api
+
+
+
+
+
+
+ org.glassfish.jersey.core
+ jersey-client
+ ${jersey.ranger.version}
+
+
+ org.glassfish.jersey.core
+ jersey-common
+ ${jersey.ranger.version}
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+ ${jersey.ranger.version}
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ ${jaxrs.api.version}
+
+
+ io.netty
+ netty-handler
+ provided
+
+
+ io.netty
+ netty-common
+ provided
+
+
+ org.apache.hadoop
+ hadoop-common
+ provided
+
+
+ commons-codec
+ commons-codec
+
+
+ org.slf4j
+ slf4j-reload4j
+
+
+ javax.servlet
+ javax.servlet-api
+
+
+ javax.servlet.jsp
+ jsp-api
+
+
+
+
+ org.slf4j
+ slf4j-api
+ provided
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+
diff --git a/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControl.java b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControl.java
new file mode 100644
index 00000000000..fa496dc70fe
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControl.java
@@ -0,0 +1,279 @@
+/*
+ * 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.ranger.authorization.drill.authorizer;
+
+
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.ranger.authorization.drill.resource.DrillAccessType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Drill-facing authorization facade: implementation of the Drill
+ * {@link AccessAuthorizer} SPI backed by Ranger.
+ *
+ * The class is instantiated once by the shim (drill-ranger-plugin-shim)
+ * via {@code DrillAccessControl(String serviceName)} — the constructor
+ * completes all initialization (or throws, fail-closed), mirroring Presto's
+ * {@code RangerPrestoAccessControl}. All access checks are instance methods
+ * invoked directly through the SPI interface.
+ *
+ * Group resolution: if the engine-supplied {@link UserIdentity} carries
+ * groups (resolved at authentication time), they are used as-is; otherwise
+ * the groups are resolved via Hadoop UGI ({@link #getUserGroups}).
+ *
+ * On Ranger evaluation error, checks return {@code false} (fail-closed).
+ * The service name identifies the Ranger service instance whose policies
+ * are evaluated.
+ */
+public class DrillAccessControl implements AccessAuthorizer {
+
+ private static final Logger logger = LoggerFactory.getLogger(DrillAccessControl.class);
+
+ private final DrillAuthorizer authorizer;
+
+ // Set of system schemas that bypass authorization (information_schema, sys, etc.)
+ // Stored in uppercase; isSystemSchema() uppercases input before lookup so the
+ // bypass is case-insensitive (e.g. "information_schema", "INFORMATION_SCHEMA",
+ // "Sys", "SYS" all match).
+ private static final Set SYSTEM_SCHEMAS = new HashSet<>(Arrays.asList(
+ "INFORMATION_SCHEMA", "SYS"
+ ));
+
+ /**
+ * Creates and initializes the Ranger Drill plugin. Completes all
+ * initialization or throws (fail-closed) — the caller (the shim) must not
+ * receive a half-initialized authorizer.
+ *
+ * @param serviceName the Ranger service instance name (must match a service created in Ranger Admin)
+ */
+ public DrillAccessControl(String serviceName) {
+ logger.info("Initializing Ranger Drill authorization plugin for service: {}", serviceName);
+ try {
+ this.authorizer = new DrillAuthorizer(serviceName);
+ logger.info("Ranger Drill authorization plugin initialized successfully");
+ } catch (Exception e) {
+ logger.error("Failed to initialize Ranger Drill plugin for service {}", serviceName, e);
+ throw new RuntimeException(
+ "Failed to initialize Ranger Drill plugin — authorization disabled " + serviceName
+ + " with exception: " + e);
+ }
+ }
+
+ /**
+ * Package-private constructor for unit tests: injects a (mock) authorizer
+ * directly, bypassing Ranger Admin connectivity.
+ */
+ DrillAccessControl(DrillAuthorizer authorizer) {
+ this.authorizer = authorizer;
+ }
+
+ /**
+ * Resolves the OS-level groups for a given user via Hadoop UGI. Used only
+ * as a fallback when the engine-supplied {@link UserIdentity} carries no
+ * groups.
+ *
+ * @param user the username
+ * @return a set of group names (never null, empty on failure)
+ */
+ public static Set getUserGroups(String user) {
+ if (user == null || user.trim().isEmpty()) {
+ return Collections.emptySet();
+ }
+ try {
+ UserGroupInformation ugi = UserGroupInformation.createRemoteUser(user);
+ String[] groups = ugi.getGroupNames();
+ return groups == null ? Collections.emptySet() : new HashSet<>(Arrays.asList(groups));
+ } catch (Exception e) {
+ logger.warn("Failed to determine groups for user={}", user, e);
+ return Collections.emptySet();
+ }
+ }
+
+ /**
+ * Checks table-level access. The SPI {@link AccessType} is mapped to
+ * {@link DrillAccessType} by name; if the mapping fails (e.g. the SPI
+ * added a type this plugin does not know yet) access is denied
+ * (fail-closed).
+ *
+ * System schemas ({@code INFORMATION_SCHEMA}, {@code sys}) bypass
+ * authorization. A null/empty schema is invalid input and fails closed
+ * rather than silently bypassing Ranger.
+ *
+ * @param user the querying user identity
+ * @param dataSource the Drill storage plugin name (e.g. "dfs", "hbase")
+ * @param schema the schema path (e.g. "dfs.tmp")
+ * @param table the table name
+ * @param accessType the access type (e.g. SELECT, CREATE)
+ * @return {@code true} if access is allowed
+ */
+ @Override
+ public boolean checkTableAccess(UserIdentity user, String dataSource, String schema,
+ String table, AccessType accessType) {
+ DrillAccessType operator = parseAccessType(accessType, user, schema, table);
+ if (operator == null) {
+ return false;
+ }
+ Boolean bypass = systemSchemaBypass(user, schema, table);
+ if (bypass != null) {
+ return bypass;
+ }
+ try {
+ return authorizer.checkTableAccess(resolveIdentity(user), dataSource, schema, table, operator);
+ } catch (Exception e) {
+ logger.error("Checking table access for user={}, schema={}, table={}. with exception:{}",
+ user.getUser(), schema, table, e.toString());
+ return false; // fail-closed on error
+ }
+ }
+
+ /**
+ * Checks column-level access for a set of columns. Returns {@code true}
+ * only if the user has the specified access type on ALL given columns.
+ *
+ * System schemas ({@code INFORMATION_SCHEMA}, {@code sys}) bypass
+ * authorization. A null/empty schema is invalid input and fails closed
+ * rather than silently bypassing Ranger.
+ *
+ * @param user the querying user identity
+ * @param dataSource the Drill storage plugin name
+ * @param schema the schema path
+ * @param table the table name
+ * @param columns the set of column names to check
+ * @param accessType the access type (e.g. SELECT)
+ * @return {@code true} if access is allowed for every column
+ */
+ @Override
+ public boolean checkColumnAccess(UserIdentity user, String dataSource, String schema,
+ String table, Set columns, AccessType accessType) {
+ DrillAccessType operator = parseAccessType(accessType, user, schema, table);
+ if (operator == null) {
+ return false;
+ }
+ Boolean bypass = systemSchemaBypass(user, schema, table);
+ if (bypass != null) {
+ return bypass;
+ }
+ try {
+ return authorizer.checkColumnAccess(resolveIdentity(user), dataSource, schema, table,
+ columns, operator);
+ } catch (Exception e) {
+ logger.error("Error checking column access for user={}, schema={}, table={}",
+ user.getUser(), schema, table, e);
+ return false; // fail-closed on error
+ }
+ }
+
+ /**
+ * Maps the SPI {@link AccessType} to a {@link DrillAccessType} by name.
+ * Returns {@code null} (and logs) when the SPI enum carries a type this
+ * plugin does not know yet — callers deny access in that case
+ * (fail-closed). This guards against drift when a newer SPI adds access
+ * types before the Ranger service-def does.
+ */
+ private DrillAccessType parseAccessType(AccessType accessType, UserIdentity user,
+ String schema, String table) {
+ try {
+ return DrillAccessType.valueOf(accessType.name());
+ } catch (Exception e) {
+ logger.error("Unsupported access type '{}', denied access for user={}, schema={}, table={}",
+ accessType, user.getUser(), schema, table);
+ return null;
+ }
+ }
+
+ /**
+ * Resolves the effective identity for an access check: engine-supplied
+ * groups are used as-is; when the identity carries none (e.g. mount points
+ * that only have the authenticated user name), the groups are resolved via
+ * Hadoop UGI ({@link #getUserGroups}).
+ */
+ private UserIdentity resolveIdentity(UserIdentity user) {
+ Set groups = user.getGroups();
+ if (groups != null && !groups.isEmpty()) {
+ return user;
+ }
+ return UserIdentity.builder()
+ .setUser(user.getUser())
+ .setGroups(getUserGroups(user.getUser()))
+ .build();
+ }
+
+ /**
+ * Returns whether the given schema is a system schema that should bypass
+ * authorization.
+ *
+ * Comparison is case-insensitive so that SQL like
+ * {@code SELECT * FROM information_schema.tables} (lowercase) or
+ * {@code SELECT * FROM SYS.DRILLBITS} (uppercase) both bypass authorization,
+ * matching Drill's own case-insensitive schema resolution.
+ *
+ *
For compound schema paths like {@code dfs.tmp}, only the top-level segment
+ * (the storage plugin name) is checked — that is intentional, because system
+ * schemas ({@code INFORMATION_SCHEMA}, {@code sys}) are always top-level.
+ */
+ private static boolean isSystemSchema(String schema) {
+ if (schema == null || schema.trim().isEmpty()) {
+ throw new IllegalArgumentException(
+ "Schema must not be null or empty for authorization check; refusing to treat as system schema");
+ }
+ // Use only the top-level segment of a compound schema path
+ // (e.g. "dfs.tmp" -> "dfs", "INFORMATION_SCHEMA" -> "INFORMATION_SCHEMA")
+ String topLevel = schema;
+ int dot = schema.indexOf('.');
+ if (dot > 0) {
+ topLevel = schema.substring(0, dot);
+ }
+ return SYSTEM_SCHEMAS.contains(topLevel.toUpperCase());
+ }
+
+ /**
+ * Shared guard for the check methods: returns {@link Boolean#TRUE} for a
+ * system schema (bypass authorization), {@link Boolean#FALSE} for a
+ * malformed (null/empty) schema (fail closed — never silently bypass
+ * Ranger), or {@code null} when the check should proceed to the authorizer.
+ */
+ private Boolean systemSchemaBypass(UserIdentity user, String schema, String table) {
+ try {
+ return isSystemSchema(schema) ? Boolean.TRUE : null;
+ } catch (IllegalArgumentException e) {
+ logger.error("Malformed (null/empty) schema in access check for user={}, table={}: {}",
+ user.getUser(), table, e.getMessage());
+ return Boolean.FALSE; // malformed schema: fail closed
+ }
+ }
+
+ /**
+ * Releases the Ranger plugin resources held by the underlying authorizer
+ * (policy-refresh threads, policy-engine caches). Forwarded from the shim
+ * ({@code RangerAccessAuthorizer}) when the Drillbit shuts down. Idempotent
+ * and safe if the plugin was never initialized.
+ */
+ @Override
+ public void close() {
+ logger.info("Closing Ranger Drill authorization plugin");
+ authorizer.close();
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizer.java b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizer.java
new file mode 100644
index 00000000000..1805ad1a210
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizer.java
@@ -0,0 +1,178 @@
+/*
+ * 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.ranger.authorization.drill.authorizer;
+
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.apache.ranger.authorization.drill.resource.DrillAccessResource;
+import org.apache.ranger.authorization.drill.resource.DrillAccessType;
+import org.apache.ranger.authorization.drill.resource.DrillRangerAccessRequest;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Ranger-side authorization adapter: builds {@link RangerAccessRequest}s from
+ * plain check parameters and evaluates them against the Ranger policy engine.
+ *
+ *
Responsibilities: input validation (fail-closed on malformed arguments),
+ * per-column request iteration, and resource-matching-scope semantics —
+ * table-level checks use {@code SELF_OR_DESCENDANTS} so a table request can
+ * match column-level policies, column-level checks use {@code SELF} for exact
+ * column matching.
+ */
+public class DrillAuthorizer {
+ private static final Logger logger = LoggerFactory.getLogger(DrillAuthorizer.class);
+ private RangerBaseAuthorizer authorizer;
+
+ public DrillAuthorizer(String serviceName) {
+ authorizer = RangerBaseAuthorizer.getInstance();
+ authorizer.init(serviceName);
+ }
+
+ /**
+ * Checks table-level access.
+ *
+ * @param user the querying user identity (with groups already resolved)
+ * @param dataSource the Drill storage plugin name (e.g. "dfs")
+ * @param schema the schema path (e.g. "dfs.tmp")
+ * @param table the table name
+ * @param operator the access type to check
+ * @return {@code true} if access is allowed; {@code false} on denial or
+ * malformed input (fail-closed)
+ */
+ public boolean checkTableAccess(UserIdentity user, String dataSource, String schema,
+ String table, DrillAccessType operator) {
+ if (!validate(user, dataSource, schema, table)) {
+ logger.warn("Table access check denied: invalid arguments for user={}, datasource={}, schema={}, table={}",
+ user == null ? null : user.getUser(), dataSource, schema, table);
+ return false;
+ }
+ DrillAccessResource resource = new DrillAccessResource(dataSource,
+ Optional.ofNullable(schema), Optional.ofNullable(table));
+
+ // Table-level check uses SELF_OR_DESCENDANTS so a request without a column
+ // can still match column-level policies (column is a descendant of table).
+ // This allows a single policy with column=amount to authorize the table-level
+ // SELECT check that happens during SQL parsing (before columns are resolved).
+ boolean result = checkAccess(user, resource, operator,
+ RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS);
+ if (logger.isDebugEnabled()) {
+ logger.debug("checkTableAccess result for user={}, datasource={}, schema={}, table={}, " +
+ "operator={}: result={}",
+ user.getUser(), dataSource, schema, table, operator.name(), result);
+ }
+ return result;
+ }
+
+ /**
+ * Checks column-level access for a set of columns. Each column is checked
+ * individually (fail-fast on the first denial).
+ *
+ * @param user the querying user identity (with groups already resolved)
+ * @param dataSource the Drill storage plugin name (e.g. "dfs")
+ * @param schema the schema path (e.g. "dfs.tmp")
+ * @param table the table name
+ * @param columns the column names to check
+ * @param operator the access type to check
+ * @return {@code true} if access is allowed for every column; {@code false}
+ * on denial or malformed input (fail-closed)
+ */
+ public boolean checkColumnAccess(UserIdentity user, String dataSource, String schema,
+ String table, Set columns, DrillAccessType operator) {
+ if (!validate(user, dataSource, schema, table) || !validColumns(columns)) {
+ logger.warn("Column access check denied: invalid arguments for user={}, datasource={}, schema={}, table={}",
+ user == null ? null : user.getUser(), dataSource, schema, table);
+ return false;
+ }
+ Optional schemaOpt = Optional.ofNullable(schema);
+ Optional tableOpt = Optional.ofNullable(table);
+
+ for (String column : columns) {
+ DrillAccessResource resource = new DrillAccessResource(dataSource,
+ schemaOpt, tableOpt, Optional.of(column));
+
+ // Column-level check uses SELF for exact column matching: only policies
+ // whose column resource matches the requested column will be applied.
+ boolean allowed = checkAccess(user, resource, operator,
+ RangerAccessRequest.ResourceMatchingScope.SELF);
+ if (logger.isDebugEnabled()) {
+ logger.debug("checkColumnAccess result for user={}, datasource={}, schema={}, table={}, " +
+ "column={}, operator={}: result={}",
+ user.getUser(), dataSource, schema, table, column, operator.name(), allowed);
+ }
+ if (!allowed) {
+ // Fail fast on first denied column — no need to check the rest.
+ logger.warn("Column access denied for user={}, column={}.{}.{}",
+ user.getUser(), dataSource, schema, table, column);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Releases the Ranger plugin resources owned by the singleton
+ * {@link RangerBaseAuthorizer} (policy-refresh threads, policy-engine
+ * caches). Idempotent; safe when the plugin was never initialized. Called
+ * through {@link DrillAccessControl#close()} when the Drillbit shuts down.
+ */
+ public void close() {
+ authorizer.cleanUp();
+ }
+
+ /**
+ * Builds a {@link DrillRangerAccessRequest} from the identity, resource and
+ * access type, then evaluates it against the Ranger policy engine.
+ */
+ private boolean checkAccess(UserIdentity user, DrillAccessResource drillAccessResource,
+ DrillAccessType operator, RangerAccessRequest.ResourceMatchingScope scope) {
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user(user.getUser())
+ .groups(user.getGroups())
+ .resource(drillAccessResource)
+ .accessType(operator)
+ .resourceMatchingScope(scope)
+ .build();
+
+ return authorizer.isAccessAllowed(request.toRangerRequest());
+ }
+
+ /**
+ * Validates the arguments shared by table- and column-level checks: the
+ * identity, its user name, dataSource, schema and table must all be
+ * non-null and non-empty (fail-closed on malformed input).
+ */
+ private boolean validate(UserIdentity user, String dataSource, String schema, String table) {
+ return user != null
+ && user.getUser() != null && !user.getUser().trim().isEmpty()
+ && dataSource != null && !dataSource.trim().isEmpty()
+ && schema != null && !schema.trim().isEmpty()
+ && table != null && !table.trim().isEmpty();
+ }
+
+ /**
+ * Validates the column set: non-null, non-empty, and every column name
+ * non-null and non-empty.
+ */
+ private boolean validColumns(Set columns) {
+ return columns != null && !columns.isEmpty()
+ && columns.stream().allMatch(c -> c != null && !c.trim().isEmpty());
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizer.java b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizer.java
new file mode 100644
index 00000000000..df25afe57ba
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizer.java
@@ -0,0 +1,118 @@
+/*
+ * 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.ranger.authorization.drill.authorizer;
+
+import org.apache.ranger.plugin.audit.RangerDefaultAuditHandler;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.apache.ranger.plugin.policyengine.RangerAccessResult;
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Singleton wrapper around {@link RangerBasePlugin} for the Drill service type.
+ *
+ * Initialized once at Drillbit startup with the service name configured in
+ * {@code ranger-drill-security.xml}. After initialization,
+ * {@link #isAccessAllowed(RangerAccessRequest)}
+ * performs local in-memory policy evaluation (policies are pulled periodically by the
+ * {@code PolicyRefresher} background thread, identical to the Hive plugin).
+ */
+public class RangerBaseAuthorizer {
+ private static final Logger logger = LoggerFactory.getLogger(RangerBaseAuthorizer.class);
+
+ private volatile RangerBasePlugin plugin;
+
+ private RangerBaseAuthorizer() {
+
+ }
+
+ private static class LazyHolder {
+ private static final RangerBaseAuthorizer INSTANCE = new RangerBaseAuthorizer();
+ }
+
+ public static RangerBaseAuthorizer getInstance() {
+ return LazyHolder.INSTANCE;
+ }
+
+ /**
+ * Initializes the Ranger plugin. The {@code serviceType} MUST be "drill" to match the
+ * service-def registered in Ranger Admin.
+ *
+ * @param serviceName the service instance name (matches {@code ranger.plugin.drill.service.name})
+ */
+ public synchronized void init(String serviceName) {
+ if (plugin != null) {
+ return;
+ }
+ try {
+ plugin = new RangerDrillPlugin(serviceName);
+ plugin.setResultProcessor(new RangerDefaultAuditHandler());
+ plugin.init();
+ logger.info("RangerPlugin initialized successfully for serviceName: {}", serviceName);
+ } catch (Exception e) {
+ plugin = null;
+ throw new RuntimeException("Failed to initialize RangerPlugin", e);
+ }
+ }
+
+ public boolean isAccessAllowed(RangerAccessRequest request) {
+ if (plugin == null) {
+ logger.error("Plugin not initialized!");
+ return false;
+ }
+ RangerAccessResult result = plugin.isAccessAllowed(request);
+ return result != null && result.getIsAllowed();
+ }
+
+ /**
+ * Forces an immediate policy refresh. Normally NOT needed — the {@code PolicyRefresher}
+ * background thread pulls policies periodically. Kept for administrative use only.
+ */
+ public void refreshPoliciesNow() {
+ if (plugin != null) {
+ plugin.refreshPoliciesAndTags();
+ }
+ }
+
+ /**
+ * Releases the underlying {@link RangerBasePlugin} resources (policy-refresh
+ * threads, policy-engine caches) and clears the plugin reference.
+ *
+ * Idempotent and thread-safe ({@code synchronized}, mutually exclusive
+ * with {@link #init(String)}): calling {@code cleanUp()} without a prior
+ * {@code init()}, or twice in a row, is a safe no-op. After this method the
+ * instance is back to its pre-init state, so a subsequent {@code init()}
+ * re-creates a fresh plugin — this matters when a Drillbit is shut down and
+ * restarted inside the same JVM.
+ */
+ public synchronized void cleanUp() {
+ if (plugin == null) {
+ return;
+ }
+ logger.info("Cleaning up RangerPlugin");
+ try {
+ plugin.cleanup();
+ } catch (Exception e) {
+ logger.warn("Error while cleaning up RangerPlugin", e);
+ } finally {
+ // Always drop the reference: even if cleanup() partially failed, a
+ // fresh plugin must be creatable on the next init().
+ plugin = null;
+ }
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerDrillPlugin.java b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerDrillPlugin.java
new file mode 100644
index 00000000000..6bc9e9bca54
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerDrillPlugin.java
@@ -0,0 +1,32 @@
+/*
+ * 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.ranger.authorization.drill.authorizer;
+
+import org.apache.ranger.plugin.service.RangerBasePlugin;
+
+public class RangerDrillPlugin extends RangerBasePlugin {
+ /**
+ * The Ranger service type. MUST match {@code "name"} in ranger-servicedef-drill.json
+ * and {@code RangerDrillPlugin.SERVICE_TYPE}.
+ */
+ public final static String SERVICE_TYPE = "drill";
+ public final static String RANGER_DRILL_APPID = "drill";
+
+ public RangerDrillPlugin(String serviceName) {
+ super(SERVICE_TYPE, serviceName, RANGER_DRILL_APPID);
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessResource.java b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessResource.java
new file mode 100644
index 00000000000..095506649d2
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessResource.java
@@ -0,0 +1,87 @@
+/*
+ * 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.ranger.authorization.drill.resource;
+
+import org.apache.ranger.plugin.policyengine.RangerAccessResourceImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Optional;
+
+public class DrillAccessResource extends RangerAccessResourceImpl {
+
+ private static final Logger logger = LoggerFactory.getLogger(DrillAccessResource.class);
+
+ public DrillAccessResource(Map> resource) {
+ super();
+ for (Map.Entry> entry : resource.entrySet()) {
+ String key = entry.getKey().toString();
+ Optional value = entry.getValue();
+ value.ifPresent(s -> this.setValue(key, s));
+ if (logger.isDebugEnabled()) {
+ logger.debug("AccessResource set value: {} = {}", key, value);
+ }
+ }
+ }
+
+ public DrillAccessResource(String dataSource, Optional schema, Optional table) {
+ setValue(RangerDrillResource.DATASOURCE.toString(), dataSource);
+ schema.ifPresent(s -> setValue(RangerDrillResource.SCHEMA.toString(), s));
+ table.ifPresent(s -> setValue(RangerDrillResource.TABLE.toString(), s));
+ }
+
+ public DrillAccessResource(String dataSource, Optional schema, Optional table,
+ Optional column) {
+ setValue(RangerDrillResource.DATASOURCE.toString(), dataSource);
+ schema.ifPresent(s -> setValue(RangerDrillResource.SCHEMA.toString(), s));
+ table.ifPresent(s -> setValue(RangerDrillResource.TABLE.toString(), s));
+ column.ifPresent(s -> setValue(RangerDrillResource.COLUMN.toString(), s));
+ }
+
+ public String getDataSource() {
+ return (String) getValue(RangerDrillResource.DATASOURCE.toString());
+ }
+
+ public String getTable() {
+ return (String) getValue(RangerDrillResource.TABLE.toString());
+ }
+
+ public String getSchema() {
+ return (String) getValue(RangerDrillResource.SCHEMA.toString());
+ }
+
+
+}
+
+enum RangerDrillResource {
+ DATASOURCE("datasource"),
+ SCHEMA("schema"),
+ TABLE("table"),
+ COLUMN("column");
+
+ private final String key;
+
+ RangerDrillResource(String key) {
+ this.key = key;
+ }
+
+ @Override
+ public String toString() {
+ return key;
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessType.java b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessType.java
new file mode 100644
index 00000000000..b98e147fe4c
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessType.java
@@ -0,0 +1,21 @@
+/*
+ * 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.ranger.authorization.drill.resource;
+
+public enum DrillAccessType {
+ CREATE, DROP, SELECT;
+}
diff --git a/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequest.java b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequest.java
new file mode 100644
index 00000000000..4381f475860
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.ranger.authorization.drill.resource;
+
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequestImpl;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+public class DrillRangerAccessRequest {
+
+ private String user;
+ private Set groups = new HashSet<>();
+ private DrillAccessResource resource;
+ private DrillAccessType accessType;
+ private String action;
+ private String clientIPAddress;
+ private String clientType;
+ // Resource matching scope. Table-level checks use SELF_OR_DESCENDANTS so a
+ // table request can match column-level policies (column is a descendant of
+ // table in the resource hierarchy). Column-level checks use SELF for exact
+ // column matching. Defaults to SELF_OR_DESCENDANTS to preserve historical
+ // behavior when the caller does not specify a scope.
+ private RangerAccessRequest.ResourceMatchingScope resourceMatchingScope =
+ RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS;
+
+ private DrillRangerAccessRequest(Builder builder) {
+ this.user = builder.user;
+ this.groups = builder.groups;
+ this.resource = builder.resource;
+ this.accessType = builder.accessType;
+ this.action = builder.action;
+ this.clientIPAddress = builder.clientIPAddress;
+ this.clientType = builder.clientType;
+ this.resourceMatchingScope = builder.resourceMatchingScope;
+ }
+
+ public RangerAccessRequest toRangerRequest() {
+ RangerAccessRequestImpl request = new RangerAccessRequestImpl();
+ request.setUser(user);
+ request.setUserGroups(groups);
+ request.setResource(resource);
+ // Access type name MUST match the service-def's accessTypes[].name exactly
+ // (Ranger matching is case-sensitive). DrillAccessType enum constants are
+ // uppercase (SELECT, CREATE, ...) and the service-def registers them as
+ // uppercase too, so we use the enum name directly — no toLowerCase().
+ request.setAccessType(accessType.name());
+ request.setAction(action != null ? action : accessType.name());
+ request.setClientIPAddress(clientIPAddress);
+ request.setClientType(clientType);
+ request.setResourceMatchingScope(resourceMatchingScope);
+
+ return request;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String user;
+ private Set groups = new HashSet<>();
+ private DrillAccessResource resource;
+ private DrillAccessType accessType;
+ private String action;
+ private String clientIPAddress;
+ private String clientType;
+ private RangerAccessRequest.ResourceMatchingScope resourceMatchingScope =
+ RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS;
+
+ public Builder user(String user) {
+ this.user = user;
+ return this;
+ }
+
+ public Builder groups(Set groups) {
+ this.groups = groups != null ? new HashSet<>(groups) : new HashSet<>();
+ return this;
+ }
+
+ public Builder addGroup(String group) {
+ this.groups.add(group);
+ return this;
+ }
+
+ public Builder resource(DrillAccessResource resource) {
+ this.resource = resource;
+ return this;
+ }
+
+ public Builder accessType(DrillAccessType accessType) {
+ this.accessType = accessType;
+ return this;
+ }
+
+ public Builder action(String action) {
+ this.action = action;
+ return this;
+ }
+
+ public Builder clientIPAddress(String clientIPAddress) {
+ this.clientIPAddress = clientIPAddress;
+ return this;
+ }
+
+ public Builder clientType(String clientType) {
+ this.clientType = clientType;
+ return this;
+ }
+
+ /**
+ * Sets the resource matching scope. Use {@code SELF_OR_DESCENDANTS} for
+ * table-level checks (so a table request can match column-level policies
+ * whose resource is a descendant of table), and {@code SELF} for exact
+ * column-level matching.
+ *
+ * @param scope the resource matching scope
+ * @return this builder
+ */
+ public Builder resourceMatchingScope(RangerAccessRequest.ResourceMatchingScope scope) {
+ this.resourceMatchingScope = scope;
+ return this;
+ }
+
+ public DrillRangerAccessRequest build() {
+ return new DrillRangerAccessRequest(this);
+ }
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControlTest.java b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControlTest.java
new file mode 100644
index 00000000000..86407c7fb46
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControlTest.java
@@ -0,0 +1,318 @@
+/*
+ * 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.ranger.authorization.drill.authorizer;
+
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.ranger.authorization.drill.resource.DrillAccessType;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for the instance-based {@link DrillAccessControl} SPI
+ * implementation. Covers system-schema bypass, fail-closed behavior
+ * (exception / malformed schema), SPI-enum-to-DrillAccessType mapping,
+ * delegation to {@link DrillAuthorizer} and user-group resolution
+ * (engine-supplied groups take precedence, UGI is the fallback).
+ */
+public class DrillAccessControlTest {
+
+ private static final String USER = "root";
+ private static final String DS = "mysql";
+ private static final String SCHEMA = "shf";
+ private static final String TABLE = "orders";
+
+ /**
+ * Class-level mock of {@link UserGroupInformation} to prevent JNI-based group
+ * lookup ({@code JniBasedUnixGroupsMapping}) which fails on Windows /
+ * non-Unix environments and pollutes test logs with IOException stacks.
+ * Opened in {@link #setUp()} and closed in {@link #tearDown()} so that every
+ * test method — including those that indirectly call {@code getUserGroups}
+ * via the group-resolution fallback — gets a deterministic empty group set
+ * without touching the OS.
+ */
+ private MockedStatic ugiMock;
+ private UserGroupInformation mockUgi;
+
+ private DrillAuthorizer mockAuthorizer;
+ private DrillAccessControl accessControl;
+
+ @BeforeEach
+ public void setUp() {
+ // Stub UGI for any user: createRemoteUser returns a mock whose
+ // getGroupNames() returns an empty array by default. Individual tests
+ // (e.g. getUserGroups_returnsNonNullForValidUser) can re-stub mockUgi
+ // to return specific groups or throw exceptions.
+ ugiMock = mockStatic(UserGroupInformation.class);
+ mockUgi = mock(UserGroupInformation.class);
+ ugiMock.when(() -> UserGroupInformation.createRemoteUser(anyString()))
+ .thenReturn(mockUgi);
+ when(mockUgi.getGroupNames()).thenReturn(new String[0]);
+
+ mockAuthorizer = mock(DrillAuthorizer.class);
+ accessControl = new DrillAccessControl(mockAuthorizer);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ if (ugiMock != null) {
+ ugiMock.close();
+ ugiMock = null;
+ }
+ }
+
+ // ========================================================================
+ // System-schema bypass
+ // ========================================================================
+
+ @Test
+ public void checkTableAccess_bypassesSystemSchema_informationSchema() {
+ assertTrue(accessControl.checkTableAccess(
+ UserIdentity.of(USER), "dfs", "INFORMATION_SCHEMA", "TABLES", AccessType.SELECT));
+ verify(mockAuthorizer, never()).checkTableAccess(any(), any(), any(), any(), any());
+ }
+
+ @Test
+ public void checkTableAccess_bypassesSystemSchema_sys_caseInsensitive() {
+ for (String schema : new String[] {"sys", "Sys", "SYS"}) {
+ assertTrue(accessControl.checkTableAccess(
+ UserIdentity.of(USER), "dfs", schema, "DRILLBITS", AccessType.SELECT),
+ "schema=" + schema + " should bypass authorization");
+ }
+ verify(mockAuthorizer, never()).checkTableAccess(any(), any(), any(), any(), any());
+ }
+
+ @Test
+ public void checkTableAccess_bypassesSystemSchema_compoundPath() {
+ // Top-level segment "information_schema" should match, even with compound path
+ assertTrue(accessControl.checkTableAccess(
+ UserIdentity.of(USER), "dfs", "information_schema.tables", "COLUMNS", AccessType.SELECT));
+ verify(mockAuthorizer, never()).checkTableAccess(any(), any(), any(), any(), any());
+ }
+
+ @Test
+ public void checkColumnAccess_bypassesSystemSchema() {
+ Set columns = new HashSet<>(Collections.singletonList("TABLE_NAME"));
+ assertTrue(accessControl.checkColumnAccess(
+ UserIdentity.of(USER), "dfs", "INFORMATION_SCHEMA", "TABLES", columns, AccessType.SELECT));
+ verify(mockAuthorizer, never()).checkColumnAccess(any(), any(), any(), any(), any(), any());
+ }
+
+ // ========================================================================
+ // Malformed schema and unknown access type: fail-closed
+ // ========================================================================
+
+ @Test
+ public void checkTableAccess_doesNotBypass_nullSchema_denied() {
+ // A null schema is not a system schema; it must fail closed (denied) rather
+ // than being silently treated as one and bypassing authorization.
+ assertFalse(accessControl.checkTableAccess(
+ UserIdentity.of(USER), "dfs", null, TABLE, AccessType.SELECT),
+ "null schema must not bypass authorization");
+ verify(mockAuthorizer, never()).checkTableAccess(any(), any(), any(), any(), any());
+ }
+
+ @Test
+ public void checkTableAccess_doesNotBypass_emptySchema_denied() {
+ // Empty/whitespace schemas are not system schemas; they must fail closed
+ // (denied) rather than silently bypassing authorization.
+ for (String schema : new String[] {"", " "}) {
+ assertFalse(accessControl.checkTableAccess(
+ UserIdentity.of(USER), "dfs", schema, TABLE, AccessType.SELECT),
+ "empty/whitespace schema must not bypass authorization, schema='" + schema + "'");
+ }
+ verify(mockAuthorizer, never()).checkTableAccess(any(), any(), any(), any(), any());
+ }
+
+ // ========================================================================
+ // Delegation to DrillAuthorizer
+ // ========================================================================
+
+ @Test
+ public void checkTableAccess_delegatesToAuthorizer() {
+ when(mockAuthorizer.checkTableAccess(any(), anyString(), anyString(), anyString(),
+ eq(DrillAccessType.SELECT))).thenReturn(true);
+
+ assertTrue(accessControl.checkTableAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, AccessType.SELECT));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(UserIdentity.class);
+ verify(mockAuthorizer).checkTableAccess(captor.capture(), eq(DS), eq(SCHEMA),
+ eq(TABLE), eq(DrillAccessType.SELECT));
+ assertEquals(USER, captor.getValue().getUser());
+ }
+
+ @Test
+ public void checkColumnAccess_delegatesToAuthorizer() {
+ when(mockAuthorizer.checkColumnAccess(any(), anyString(), anyString(), anyString(),
+ any(), eq(DrillAccessType.SELECT))).thenReturn(false);
+
+ Set columns = new HashSet<>(Arrays.asList("id", "amount"));
+ assertFalse(accessControl.checkColumnAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, columns, AccessType.SELECT));
+
+ ArgumentCaptor identityCaptor = ArgumentCaptor.forClass(UserIdentity.class);
+ ArgumentCaptor> columnsCaptor = ArgumentCaptor.forClass(Set.class);
+ verify(mockAuthorizer).checkColumnAccess(identityCaptor.capture(), eq(DS), eq(SCHEMA),
+ eq(TABLE), columnsCaptor.capture(), eq(DrillAccessType.SELECT));
+ assertEquals(USER, identityCaptor.getValue().getUser());
+ assertEquals(columns, columnsCaptor.getValue());
+ }
+
+ @Test
+ public void checkTableAccess_mapsSpiEnumToDrillAccessType_byName() {
+ // Pins the name-based mapping: every SPI AccessType the engine can emit
+ // must have a DrillAccessType with the same name, otherwise the check
+ // fails closed at run time.
+ when(mockAuthorizer.checkTableAccess(any(), anyString(), anyString(), anyString(), any()))
+ .thenReturn(true);
+
+ for (AccessType type : AccessType.values()) {
+ assertTrue(accessControl.checkTableAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, type),
+ "SPI AccessType." + type.name() + " must map to a DrillAccessType");
+ verify(mockAuthorizer).checkTableAccess(any(), eq(DS), eq(SCHEMA), eq(TABLE),
+ eq(DrillAccessType.valueOf(type.name())));
+ }
+ }
+
+ @Test
+ public void checkTableAccess_returnsFalse_whenAuthorizerThrows() {
+ when(mockAuthorizer.checkTableAccess(any(), anyString(), anyString(), anyString(), any()))
+ .thenThrow(new RuntimeException("boom"));
+
+ assertFalse(accessControl.checkTableAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, AccessType.SELECT));
+ }
+
+ @Test
+ public void checkColumnAccess_returnsFalse_whenAuthorizerThrows() {
+ when(mockAuthorizer.checkColumnAccess(any(), anyString(), anyString(), anyString(), any(), any()))
+ .thenThrow(new RuntimeException("boom"));
+
+ Set columns = new HashSet<>(Collections.singletonList("amount"));
+ assertFalse(accessControl.checkColumnAccess(
+ UserIdentity.of(USER), DS, SCHEMA, TABLE, columns, AccessType.SELECT));
+ }
+
+ // ========================================================================
+ // Group resolution: engine-supplied groups win, UGI is the fallback
+ // ========================================================================
+
+ @Test
+ public void checkTableAccess_usesEngineSuppliedGroups() {
+ Set engineGroups = new HashSet<>(Arrays.asList("analysts", "etl"));
+ UserIdentity identity = UserIdentity.builder()
+ .setUser(USER)
+ .setGroups(engineGroups)
+ .build();
+
+ accessControl.checkTableAccess(identity, DS, SCHEMA, TABLE, AccessType.SELECT);
+
+ // The identity is passed through as-is (engine groups already present)
+ ArgumentCaptor captor = ArgumentCaptor.forClass(UserIdentity.class);
+ verify(mockAuthorizer).checkTableAccess(captor.capture(), eq(DS), eq(SCHEMA),
+ eq(TABLE), eq(DrillAccessType.SELECT));
+ assertEquals(engineGroups, captor.getValue().getGroups());
+ }
+
+ @Test
+ public void checkTableAccess_fallsBackToUgiGroups_whenIdentityHasNone() {
+ // UserIdentity.of() carries no groups → resolveIdentity falls back to UGI.
+ // Re-stub mockUgi to return specific groups and verify they reach the authorizer.
+ when(mockUgi.getGroupNames()).thenReturn(new String[] {"root", "wheel"});
+
+ accessControl.checkTableAccess(UserIdentity.of(USER), DS, SCHEMA, TABLE, AccessType.SELECT);
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(UserIdentity.class);
+ verify(mockAuthorizer).checkTableAccess(captor.capture(), eq(DS), eq(SCHEMA),
+ eq(TABLE), eq(DrillAccessType.SELECT));
+ assertEquals(new HashSet<>(Arrays.asList("root", "wheel")), captor.getValue().getGroups());
+ }
+
+ // ========================================================================
+ // getUserGroups (UGI resolution helper)
+ // ========================================================================
+
+ @Test
+ public void getUserGroups_returnsEmptyForNullUser() {
+ assertEquals(Collections.emptySet(), DrillAccessControl.getUserGroups(null));
+ }
+
+ @Test
+ public void getUserGroups_returnsEmptyForEmptyUser() {
+ assertEquals(Collections.emptySet(), DrillAccessControl.getUserGroups(""));
+ assertEquals(Collections.emptySet(), DrillAccessControl.getUserGroups(" "));
+ }
+
+ @Test
+ public void getUserGroups_returnsNonNullForValidUser() {
+ // Re-stub the class-level mockUgi to return specific groups, then verify
+ // getUserGroups propagates them correctly.
+ when(mockUgi.getGroupNames()).thenReturn(new String[] {"root", "wheel"});
+
+ Set groups = DrillAccessControl.getUserGroups(USER);
+ assertNotNull(groups);
+ assertEquals(new HashSet<>(Arrays.asList("root", "wheel")), groups);
+ }
+
+ @Test
+ public void getUserGroups_returnsEmptySet_whenUgiThrows() {
+ // Verifies the catch-block fallback: when UGI lookup throws, the method
+ // returns an empty set instead of propagating the exception.
+ when(mockUgi.getGroupNames()).thenThrow(new RuntimeException("ugi boom"));
+
+ Set groups = DrillAccessControl.getUserGroups(USER);
+ assertNotNull(groups);
+ assertTrue(groups.isEmpty());
+ }
+
+ // ========================================================================
+ // Shutdown lifecycle
+ // ========================================================================
+
+ @Test
+ public void close_releasesDelegateAuthorizer() {
+ DrillAccessControl accessControl = new DrillAccessControl(mockAuthorizer);
+
+ accessControl.close();
+
+ verify(mockAuthorizer).close();
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizerTest.java b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizerTest.java
new file mode 100644
index 00000000000..9f5040fea9a
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizerTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.ranger.authorization.drill.authorizer;
+
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.apache.ranger.authorization.drill.resource.DrillAccessType;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.MockedStatic;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link DrillAuthorizer}.
+ * Covers argument validation (fail-closed), request building (user, groups,
+ * resource values, matching scope), and per-column fail-fast behavior.
+ */
+public class DrillAuthorizerTest {
+
+ private static final String USER = "root";
+ private static final String DS = "mysql";
+ private static final String SCHEMA = "shf";
+ private static final String TABLE = "orders";
+ private static final Set GROUPS =
+ Collections.singleton("analysts");
+
+ /**
+ * Creates a DrillAuthorizer with the singleton {@link RangerBaseAuthorizer}
+ * mocked out, so the constructor does not actually contact Ranger Admin.
+ */
+ private DrillAuthorizer newAuthorizer(RangerBaseAuthorizer mockBase) {
+ try (MockedStatic mocked = mockStatic(RangerBaseAuthorizer.class)) {
+ mocked.when(RangerBaseAuthorizer::getInstance).thenReturn(mockBase);
+ return new DrillAuthorizer("svc");
+ }
+ }
+
+ private UserIdentity identity() {
+ return UserIdentity.builder().setUser(USER).setGroups(GROUPS).build();
+ }
+
+ // ========================================================================
+ // Argument validation: fail-closed, no Ranger call
+ // ========================================================================
+
+ @Test
+ public void checkTableAccess_returnsFalse_whenUserNull() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+ assertFalse(authorizer.checkTableAccess(null, DS, SCHEMA, TABLE, DrillAccessType.SELECT));
+ verify(mockBase, never()).isAccessAllowed(any());
+ }
+
+ @Test
+ public void checkTableAccess_returnsFalse_whenArgumentsEmpty() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+ UserIdentity user = identity();
+
+ assertFalse(authorizer.checkTableAccess(user, null, SCHEMA, TABLE, DrillAccessType.SELECT));
+ assertFalse(authorizer.checkTableAccess(user, "", SCHEMA, TABLE, DrillAccessType.SELECT));
+ assertFalse(authorizer.checkTableAccess(user, DS, null, TABLE, DrillAccessType.SELECT));
+ assertFalse(authorizer.checkTableAccess(user, DS, " ", TABLE, DrillAccessType.SELECT));
+ assertFalse(authorizer.checkTableAccess(user, DS, SCHEMA, null, DrillAccessType.SELECT));
+ assertFalse(authorizer.checkTableAccess(user, DS, SCHEMA, "", DrillAccessType.SELECT));
+ verify(mockBase, never()).isAccessAllowed(any());
+ }
+
+ @Test
+ public void checkColumnAccess_returnsFalse_whenColumnsInvalid() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+ UserIdentity user = identity();
+
+ // null / empty / blank-element column sets all fail closed
+ assertFalse(authorizer.checkColumnAccess(user, DS, SCHEMA, TABLE, null, DrillAccessType.SELECT));
+ assertFalse(authorizer.checkColumnAccess(user, DS, SCHEMA, TABLE,
+ Collections.emptySet(), DrillAccessType.SELECT));
+ assertFalse(authorizer.checkColumnAccess(user, DS, SCHEMA, TABLE,
+ Collections.singleton(""), DrillAccessType.SELECT));
+ assertFalse(authorizer.checkColumnAccess(user, DS, SCHEMA, TABLE,
+ Collections.singleton(" "), DrillAccessType.SELECT));
+ verify(mockBase, never()).isAccessAllowed(any());
+ }
+
+ // ========================================================================
+ // Request building: delegation with correct scope and values
+ // ========================================================================
+
+ @Test
+ public void checkTableAccess_buildsRequest_withSelfOrDescendantsScope() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ when(mockBase.isAccessAllowed(any())).thenReturn(true);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ assertTrue(authorizer.checkTableAccess(identity(), DS, SCHEMA, TABLE, DrillAccessType.SELECT));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(RangerAccessRequest.class);
+ verify(mockBase).isAccessAllowed(captor.capture());
+ RangerAccessRequest captured = captor.getValue();
+ assertEquals(RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS,
+ captured.getResourceMatchingScope());
+ assertEquals(USER, captured.getUser());
+ assertEquals(GROUPS, captured.getUserGroups());
+ assertEquals("SELECT", captured.getAccessType());
+ }
+
+ @Test
+ public void checkColumnAccess_buildsRequest_withSelfScope() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ when(mockBase.isAccessAllowed(any())).thenReturn(true);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ assertTrue(authorizer.checkColumnAccess(identity(), DS, SCHEMA, TABLE,
+ Collections.singleton("amount"), DrillAccessType.SELECT));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(RangerAccessRequest.class);
+ verify(mockBase).isAccessAllowed(captor.capture());
+ assertEquals(RangerAccessRequest.ResourceMatchingScope.SELF,
+ captor.getValue().getResourceMatchingScope());
+ }
+
+ // ========================================================================
+ // Per-column iteration: fail-fast
+ // ========================================================================
+
+ @Test
+ public void checkColumnAccess_checksEachColumnIndividually_failFast() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ // First column allowed, second column denied — fail fast on second
+ when(mockBase.isAccessAllowed(any())).thenReturn(true, false);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ // LinkedHashSet for deterministic iteration order
+ Set columns = new LinkedHashSet<>(Arrays.asList("c1", "c2", "c3"));
+ assertFalse(authorizer.checkColumnAccess(identity(), DS, SCHEMA, TABLE, columns,
+ DrillAccessType.SELECT));
+ // c1 (true) + c2 (false) -> 2 invocations; c3 should NOT be reached
+ verify(mockBase, times(2)).isAccessAllowed(any());
+ }
+
+ @Test
+ public void checkColumnAccess_allColumnsAllowed_returnsTrue() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ when(mockBase.isAccessAllowed(any())).thenReturn(true);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ Set columns = new LinkedHashSet<>(Arrays.asList("c1", "c2", "c3"));
+ assertTrue(authorizer.checkColumnAccess(identity(), DS, SCHEMA, TABLE, columns,
+ DrillAccessType.SELECT));
+ verify(mockBase, times(3)).isAccessAllowed(any());
+ }
+
+ // ========================================================================
+ // Shutdown lifecycle
+ // ========================================================================
+
+ @Test
+ public void close_invokesBaseAuthorizerCleanUp() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ authorizer.close();
+
+ verify(mockBase).cleanUp();
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizerTest.java b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizerTest.java
new file mode 100644
index 00000000000..c75809ed0f9
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizerTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.ranger.authorization.drill.authorizer;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedConstruction;
+
+import java.lang.reflect.Field;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Unit tests for the lifecycle of {@link RangerBaseAuthorizer}, the singleton
+ * wrapper around {@link RangerDrillPlugin}.
+ *
+ * The tests operate on the real singleton and reset its plugin field before
+ * (and after) each test so state never leaks across cases. No Ranger Admin or
+ * network is involved: the plugin is either a Mockito mock injected via
+ * reflection, or a mock produced by {@link MockedConstruction} for the
+ * {@code init()} path.
+ */
+public class RangerBaseAuthorizerTest {
+
+ private final RangerBaseAuthorizer authorizer = RangerBaseAuthorizer.getInstance();
+
+ @BeforeEach
+ @AfterEach
+ public void resetPluginField() throws Exception {
+ setPluginField(null);
+ }
+
+ private void setPluginField(RangerDrillPlugin plugin) throws Exception {
+ Field f = RangerBaseAuthorizer.class.getDeclaredField("plugin");
+ f.setAccessible(true);
+ f.set(authorizer, plugin);
+ }
+
+ private RangerDrillPlugin getPluginField() throws Exception {
+ Field f = RangerBaseAuthorizer.class.getDeclaredField("plugin");
+ f.setAccessible(true);
+ return (RangerDrillPlugin) f.get(authorizer);
+ }
+
+ @Test
+ public void cleanUp_releasesPluginAndClearsField() throws Exception {
+ RangerDrillPlugin plugin = mock(RangerDrillPlugin.class);
+ setPluginField(plugin);
+
+ authorizer.cleanUp();
+
+ verify(plugin).cleanup();
+ assertNull(getPluginField(),
+ "cleanUp() must drop the plugin reference so a later init() can re-create it");
+ }
+
+ @Test
+ public void cleanUp_isIdempotent() throws Exception {
+ RangerDrillPlugin plugin = mock(RangerDrillPlugin.class);
+ setPluginField(plugin);
+
+ authorizer.cleanUp();
+ authorizer.cleanUp();
+
+ verify(plugin, times(1)).cleanup();
+ assertNull(getPluginField());
+ }
+
+ @Test
+ public void cleanUp_withoutInit_isSafe() {
+ // Plugin field is null (never initialized, or already cleaned up):
+ // cleanUp() must be a safe no-op.
+ authorizer.cleanUp();
+ }
+
+ /**
+ * After a cleanUp() the plugin field is null, so the next init() must
+ * construct a fresh plugin instead of early-returning. Verified with a
+ * {@link MockedConstruction} so no real Ranger Admin connection is
+ * attempted.
+ */
+ @Test
+ public void initAfterCleanUp_createsFreshPlugin() {
+ try (MockedConstruction mocked =
+ mockConstruction(RangerDrillPlugin.class)) {
+ authorizer.init("svc-after-cleanup");
+
+ RangerDrillPlugin constructed = mocked.constructed().get(0);
+ assertNotNull(getPluginField());
+ verify(constructed).init();
+ } catch (Exception e) {
+ throw new AssertionError("init() must succeed after cleanUp()", e);
+ }
+ }
+
+ /**
+ * While a plugin is still present (not yet cleaned up), a second init()
+ * must early-return and NOT construct another plugin.
+ */
+ @Test
+ public void init_withActivePlugin_isNoOp() {
+ try (MockedConstruction mocked =
+ mockConstruction(RangerDrillPlugin.class)) {
+ authorizer.init("svc-1");
+ authorizer.init("svc-2");
+
+ // Only one plugin constructed; the second init() early-returned.
+ assertEquals(1, mocked.constructed().size());
+ } catch (Exception e) {
+ throw new AssertionError("init() must not fail", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillAccessResourceTest.java b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillAccessResourceTest.java
new file mode 100644
index 00000000000..73b7228f1f0
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillAccessResourceTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.ranger.authorization.drill.resource;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Unit tests for {@link DrillAccessResource} construction and getters.
+ * Verifies that the resource keys are lowercase and that empty
+ * {@code Optional} values do not register a key.
+ */
+public class DrillAccessResourceTest {
+
+ @Test
+ public void constructor_threeArgs_setsDatasourceSchemaTable() {
+ DrillAccessResource r = new DrillAccessResource(
+ "mysql", Optional.of("shf"), Optional.of("orders"));
+ assertEquals("mysql", r.getDataSource());
+ assertEquals("shf", r.getSchema());
+ assertEquals("orders", r.getTable());
+ }
+
+ @Test
+ public void constructor_fourArgs_setsAllKeys() {
+ DrillAccessResource r = new DrillAccessResource(
+ "mysql", Optional.of("shf"), Optional.of("orders"), Optional.of("amount"));
+ assertEquals("mysql", r.getDataSource());
+ assertEquals("shf", r.getSchema());
+ assertEquals("orders", r.getTable());
+ assertEquals("amount", r.getValue("column"));
+ }
+
+ @Test
+ public void constructor_emptySchema_doesNotSetSchemaKey() {
+ DrillAccessResource r = new DrillAccessResource(
+ "mysql", Optional.empty(), Optional.of("orders"));
+ assertNull(r.getSchema());
+ assertNotNull(r.getDataSource());
+ assertNotNull(r.getTable());
+ }
+
+ @Test
+ public void constructor_emptyTable_doesNotSetTableKey() {
+ DrillAccessResource r = new DrillAccessResource(
+ "mysql", Optional.of("shf"), Optional.empty());
+ assertEquals("mysql", r.getDataSource());
+ assertEquals("shf", r.getSchema());
+ assertNull(r.getTable());
+ }
+
+ @Test
+ public void getCatalogName_returnsDatasource() {
+ DrillAccessResource r = new DrillAccessResource(
+ "dfs", Optional.of("tmp"), Optional.of("t1"));
+ assertEquals("dfs", r.getDataSource());
+ }
+
+ @Test
+ public void getTable_returnsTableValue() {
+ DrillAccessResource r = new DrillAccessResource(
+ "dfs", Optional.of("tmp"), Optional.of("t1"));
+ assertEquals("t1", r.getTable());
+ }
+
+ @Test
+ public void getSchema_returnsSchemaValue() {
+ DrillAccessResource r = new DrillAccessResource(
+ "dfs", Optional.of("tmp"), Optional.of("t1"));
+ assertEquals("tmp", r.getSchema());
+ }
+
+ @Test
+ public void resourceKeys_areLowercase() {
+ DrillAccessResource r = new DrillAccessResource(
+ "mysql", Optional.of("shf"), Optional.of("orders"), Optional.of("amount"));
+ // Ranger requires resource keys to be lowercase; verify that lookups
+ // with lowercase keys return the registered values.
+ assertEquals("mysql", r.getValue("datasource"));
+ assertEquals("shf", r.getValue("schema"));
+ assertEquals("orders", r.getValue("table"));
+ assertEquals("amount", r.getValue("column"));
+ }
+}
diff --git a/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequestTest.java b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequestTest.java
new file mode 100644
index 00000000000..5ee53800118
--- /dev/null
+++ b/auth/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequestTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.ranger.authorization.drill.resource;
+
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link DrillRangerAccessRequest} builder and
+ * {@link DrillRangerAccessRequest#toRangerRequest()} field mapping.
+ */
+public class DrillRangerAccessRequestTest {
+
+ private DrillAccessResource buildResource() {
+ return new DrillAccessResource(
+ "mysql",
+ java.util.Optional.of("shf"),
+ java.util.Optional.of("orders"));
+ }
+
+ @Test
+ public void builder_setsAllFields() {
+ Set groups = new HashSet<>();
+ groups.add("g1");
+ DrillAccessResource resource = buildResource();
+
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user("root")
+ .groups(groups)
+ .resource(resource)
+ .accessType(DrillAccessType.SELECT)
+ .action("select")
+ .clientIPAddress("10.0.0.1")
+ .clientType("drill-jdbc")
+ .build();
+
+ RangerAccessRequest rangerRequest = request.toRangerRequest();
+ assertEquals("root", rangerRequest.getUser());
+ assertTrue(rangerRequest.getUserGroups().contains("g1"));
+ assertEquals(resource, rangerRequest.getResource());
+ assertEquals("SELECT", rangerRequest.getAccessType());
+ assertEquals("select", rangerRequest.getAction());
+ assertEquals("10.0.0.1", rangerRequest.getClientIPAddress());
+ assertEquals("drill-jdbc", rangerRequest.getClientType());
+ }
+
+ @Test
+ public void builder_defaultScopeIsSelfOrDescendants() {
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user("root")
+ .resource(buildResource())
+ .accessType(DrillAccessType.SELECT)
+ .build();
+ assertEquals(
+ RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS,
+ request.toRangerRequest().getResourceMatchingScope());
+ }
+
+ @Test
+ public void builder_explicitScope_isApplied() {
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user("root")
+ .resource(buildResource())
+ .accessType(DrillAccessType.SELECT)
+ .resourceMatchingScope(RangerAccessRequest.ResourceMatchingScope.SELF)
+ .build();
+ assertEquals(
+ RangerAccessRequest.ResourceMatchingScope.SELF,
+ request.toRangerRequest().getResourceMatchingScope());
+ }
+
+ @Test
+ public void builder_addGroup_accumulatesGroups() {
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user("root")
+ .resource(buildResource())
+ .accessType(DrillAccessType.SELECT)
+ .addGroup("g1")
+ .addGroup("g2")
+ .build();
+
+ Set groups = request.toRangerRequest().getUserGroups();
+ assertTrue(groups.contains("g1"));
+ assertTrue(groups.contains("g2"));
+ assertEquals(2, groups.size());
+ }
+
+ @Test
+ public void builder_nullGroups_createsEmptySet() {
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user("root")
+ .resource(buildResource())
+ .accessType(DrillAccessType.SELECT)
+ .groups(null)
+ .build();
+
+ Set groups = request.toRangerRequest().getUserGroups();
+ assertNotNull(groups);
+ assertTrue(groups.isEmpty());
+ }
+
+ @Test
+ public void toRangerRequest_accessTypeIsUppercase() {
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user("root")
+ .resource(buildResource())
+ .accessType(DrillAccessType.SELECT)
+ .build();
+ assertEquals("SELECT", request.toRangerRequest().getAccessType());
+ }
+
+ @Test
+ public void toRangerRequest_actionDefaultsToAccessTypeName() {
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user("root")
+ .resource(buildResource())
+ .accessType(DrillAccessType.SELECT)
+ .build();
+ assertEquals("SELECT", request.toRangerRequest().getAction());
+ }
+
+ @Test
+ public void toRangerRequest_setsResourceMatchingScope() {
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user("root")
+ .resource(buildResource())
+ .accessType(DrillAccessType.SELECT)
+ .resourceMatchingScope(RangerAccessRequest.ResourceMatchingScope.SELF)
+ .build();
+ assertEquals(
+ RangerAccessRequest.ResourceMatchingScope.SELF,
+ request.toRangerRequest().getResourceMatchingScope());
+ }
+}
diff --git a/auth/pom.xml b/auth/pom.xml
new file mode 100644
index 00000000000..6f1da48509e
--- /dev/null
+++ b/auth/pom.xml
@@ -0,0 +1,79 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.drill
+ drill-root
+ 1.23.0-SNAPSHOT
+
+
+
+ auth-parent
+ pom
+ Drill : Auth Integration Parent
+
+ Parent module aggregating authorization implementations of the
+ AccessAuthorizer SPI. Currently hosts the Drill Ranger plugin shim
+ (drill-ranger-plugin-shim) and the Drill Ranger authorization plugin
+ (drill-ranger-plugin).
+
+
+
+ 2.9.0
+
+ 2.35
+
+
+
+ drill-ranger-plugin-shim
+ drill-ranger-plugin
+
+
diff --git a/distribution/pom.xml b/distribution/pom.xml
index 10e27292638..ac052af209d 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -35,6 +35,15 @@
1.12.367
3.3.1.0.3.6
+
+ ${project.build.directory}/ranger-drill-plugin-impl.disabled
@@ -68,6 +77,16 @@
drill-java-exec
${project.version}
+
+
+ org.apache.drill
+ drill-security-spi
+ ${project.version}
+
org.apache.drill
drill-common
@@ -361,6 +380,221 @@
+
+
+ ranger
+
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+
+
+ org.apache.drill
+ drill-ranger-plugin-shim
+ ${project.version}
+
+
+
+ org.apache.drill
+ drill-ranger-plugin
+ ${project.version}
+
+
+ org.glassfish.jersey.core
+ *
+
+
+ org.glassfish.jersey.inject
+ *
+
+
+ org.glassfish.hk2
+ *
+
+
+ org.glassfish.hk2.external
+ *
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-ranger-plugin-isolated-deps
+ prepare-package
+
+ copy
+
+
+
+
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ ${jaxrs.api.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+
+ org.glassfish.jersey.core
+ jersey-client
+ ${jersey.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.jersey.core
+ jersey-common
+ ${jersey.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.jersey.inject
+ jersey-hk2
+ ${jersey.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.jersey.core
+ jersey-server
+ ${jersey.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.jersey.media
+ jersey-media-json-jackson
+ ${jersey.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.jersey.ext
+ jersey-entity-filtering
+ ${jersey.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+
+ org.glassfish.hk2
+ hk2-locator
+ ${hk2.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.hk2
+ hk2-api
+ ${hk2.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.hk2
+ hk2-utils
+ ${hk2.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.hk2.external
+ aopalliance-repackaged
+ ${hk2.ranger.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ org.glassfish.hk2
+ osgi-resource-locator
+ ${osgi.resource.locator.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+ ${jakarta.annotation.api.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+ jakarta.inject
+ jakarta.inject-api
+ ${jakarta.inject.api.version}
+ jar
+ ${project.build.directory}/ranger-drill-plugin-impl
+
+
+
+
+
+
+
+
+
+
build-jdbc-all
diff --git a/distribution/src/assemble/component.xml b/distribution/src/assemble/component.xml
index 71974cdd127..e98a98fd1a2 100644
--- a/distribution/src/assemble/component.xml
+++ b/distribution/src/assemble/component.xml
@@ -65,6 +65,8 @@
org.apache.drill:drill-common:jar
org.apache.drill:drill-logical:jar
org.apache.drill:drill-protocol:jar
+ org.apache.drill:drill-ranger-plugin-shim:jar
+ org.apache.drill:drill-security-spi:jar
org.apache.drill.exec:drill-java-exec:jar
org.apache.drill.exec:drill-jdbc:jar
org.apache.drill.exec:drill-rpc:jar
@@ -98,6 +100,61 @@
false
+
+
+
+
+ org.apache.drill:drill-ranger-plugin:jar
+
+ org.apache.ranger:ranger-plugins-common:jar
+ org.apache.ranger:ranger-knox-plugin:jar
+ org.apache.ranger:ranger-audit-core:jar
+ org.apache.ranger:ranger-audit-dest-log4j:jar
+ org.apache.ranger:ranger-authz-api:jar
+ org.apache.ranger:ranger-common-utils:jar
+ org.apache.ranger:ugsync-util:jar
+
+ jars/ranger-drill-plugin-impl
+ false
+ false
+
+
+
+
+
+ org.apache.ranger:ranger-plugin-classloader:jar
+
+ jars/3rdparty
+ false
+ false
+ compile
+
+
jars/classb
false
@@ -125,6 +182,22 @@
org.jvnet.mimepull
org.reflections
+
+
+ org.glassfish.jersey.core:jersey-client:jar:${jersey.ranger.version}
+ org.glassfish.jersey.core:jersey-common:jar:${jersey.ranger.version}
+ org.glassfish.jersey.core:jersey-server:jar:${jersey.ranger.version}
+ org.glassfish.jersey.inject:jersey-hk2:jar:${jersey.ranger.version}
+ org.glassfish.jersey.media:jersey-media-json-jackson:jar:${jersey.ranger.version}
+ org.glassfish.jersey.media:jersey-media-multipart:jar:${jersey.ranger.version}
+ org.glassfish.jersey.ext:jersey-entity-filtering:jar:${jersey.ranger.version}
+ jakarta.ws.rs:jakarta.ws.rs-api:jar:${jaxrs.api.version}
+
jars/3rdparty/
@@ -152,10 +225,16 @@
org.apache.drill.exec
org.apache.drill.memory
org.apache.drill.metastore
+
+ org.apache.ranger
+ org.glassfish.hk2.external
+ org.glassfish.hk2
org.apache.zookeeper
org.eclipse.jetty
- org.glassfish.hk2
- org.glassfish.hk2.external
org.glassfish.jersey.containers
org.glassfish.jersey.core
org.glassfish.jersey.ext
@@ -216,10 +295,27 @@
../sample-data
sample-data
+
+
+ ${ranger.impl.source.dir}
+ jars/ranger-drill-plugin-impl
+
${project.build.directory}/winutils
winutils/bin
+
+ src/main/resources/ranger
+ conf/ranger
+ 0640
+
diff --git a/distribution/src/main/resources/drill-config.sh b/distribution/src/main/resources/drill-config.sh
index 8037a8faea6..51b8720c15d 100644
--- a/distribution/src/main/resources/drill-config.sh
+++ b/distribution/src/main/resources/drill-config.sh
@@ -365,6 +365,11 @@ export DRILLBIT_LOG_PATH="${DRILL_LOG_PREFIX}.log"
# Add Drill conf folder at the beginning of the classpath
CP="$DRILL_CONF_DIR"
+# Add Ranger config directory if it exists (for ranger-drill-security.xml etc.)
+if [ -d "$DRILL_CONF_DIR/ranger" ]; then
+ CP="$CP:$DRILL_CONF_DIR/ranger"
+fi
+
# If both user and YARN-provided Java lib paths exist,
# combine them.
diff --git a/distribution/src/main/resources/logback.xml b/distribution/src/main/resources/logback.xml
index 16bf2c7b7e0..ca7e7111210 100644
--- a/distribution/src/main/resources/logback.xml
+++ b/distribution/src/main/resources/logback.xml
@@ -73,6 +73,14 @@
+
+
+
+
+
diff --git a/distribution/src/main/resources/ranger/ranger-drill-audit.xml b/distribution/src/main/resources/ranger/ranger-drill-audit.xml
new file mode 100644
index 00000000000..19369073054
--- /dev/null
+++ b/distribution/src/main/resources/ranger/ranger-drill-audit.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+ xasecure.audit.is.enabled
+ true
+
+
+
+
+ xasecure.audit.log4j.is.enabled
+ true
+
+
+
+
+ xasecure.audit.solr.is.enabled
+ false
+
+
+ xasecure.audit.solr.url
+ http://ranger-admin-host:6083/solr/ranger_audits
+
+
+
+
+ xasecure.audit.hdfs.is.enabled
+ false
+
+
+ xasecure.audit.hdfs.config.directory
+ hdfs://namenode:8020/ranger/audit
+
+
+ xasecure.audit.hdfs.config.file
+ /etc/hadoop/conf/core-site.xml
+
+
diff --git a/distribution/src/main/resources/ranger/ranger-drill-security.xml b/distribution/src/main/resources/ranger/ranger-drill-security.xml
new file mode 100644
index 00000000000..70a7096c3c4
--- /dev/null
+++ b/distribution/src/main/resources/ranger/ranger-drill-security.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ ranger.plugin.drill.policy.rest.url
+ http://ranger-admin-host:6080
+
+
+
+
+ ranger.plugin.drill.service.name
+ drill
+
+
+
+
+ ranger.plugin.drill.policy.source.impl
+ org.apache.ranger.admin.client.RangerAdminJersey2RESTClient
+
+
+
+
+ ranger.plugin.drill.policy.pollIntervalMs
+ 30000
+
+
+
+
+ ranger.plugin.drill.policy.cache.dir
+ /tmp/ranger/drill/policy
+
+
diff --git a/docs/dev/DevDocs.md b/docs/dev/DevDocs.md
index eef8105b4d6..8c2df97c6f7 100644
--- a/docs/dev/DevDocs.md
+++ b/docs/dev/DevDocs.md
@@ -27,3 +27,10 @@ For information about the Jetty 12 upgrade, known limitations, and developer gui
## Materialized Views
For information about materialized view support, including SQL syntax, query rewriting, and metastore integration, see [MaterializedViews.md](MaterializedViews.md)
+
+For information on the Jetty 12 upgrade, known limitations, and developer guidelines see [Jetty12Migration.md](Jetty12Migration.md)
+
+## Ranger Authorization
+
+For information on developing and configuring Apache Ranger column-level authorization for Drill see [RangerAuthorization.md](RangerAuthorization.md)
+
diff --git a/docs/dev/RangerAuthorization.md b/docs/dev/RangerAuthorization.md
new file mode 100644
index 00000000000..09126685521
--- /dev/null
+++ b/docs/dev/RangerAuthorization.md
@@ -0,0 +1,383 @@
+# Drill Ranger Authorization Quick Start Guide
+
+This document describes the architecture, configuration, and development
+conventions of the Apache Ranger authorization integration for Drill. It is
+intended for contributors who want to extend or debug the Ranger integration,
+and for operators who want to understand the column-level authorization
+behavior end-to-end.
+
+## 1. Architecture Overview
+
+The Ranger integration spans three layers:
+
+```
++--------------------------------------------------------------+
+| exec/java-exec (Drillbit, JDK 11) |
+| +-------------------------+ +------------------------+ |
+| | SqlConverter (toRel) | ---> | ColumnAccessChecker | |
+| | DrillCalciteCatalogReader| | (RelShuttle, column) | |
+| | Drillbit (startup) | +------------------------+ |
+| +-------------------------+ | |
+| v |
+| +-------------------------------+ +-----------------------+ |
+| | AccessAuthorizerFactory | | DrillAccessControl | |
+| | (singleton, config-driven) | | (SPI implementation) | |
+| +-------------------------------+ +-----------------------+ |
+| | |
+| +----------------------------------------------------------------+
+| | drill-ranger-plugin (JDK 11, deployed to jars/3rdparty/) |
+| | DrillAuthorizer DrillAccessResource DrillRangerAccessRequest|
+| | RangerDrillPlugin RangerBaseAuthorizer |
+| +----------------------------------------------------------------+
+```
+
+The Ranger Admin-side service plugin (`validateConfig` / `lookupResource`,
+which talks to the Drill REST API `POST /query.json`) is implemented directly
+in the Ranger codebase, not in this repository.
+
+### 1.1 Modules
+
+| Module | JDK | Deployed To | Responsibility |
+| --------------------- | --- | ------------------------- | ------------------------------------------------------------------------------------------------- |
+| `drill-ranger-plugin` | 11 | Drillbit `jars/3rdparty/` | Drillbit-side authorization: wraps `RangerBasePlugin`, exposes `DrillAccessControl` facade |
+| `exec/security-spi` | 11 | Drillbit `jars/` | JDK-only authorization SPI (`AccessAuthorizer`, `UserIdentity`, ...); shared by engine and plugin |
+| `exec/java-exec` | 11 | Drillbit | Integration hooks: `AccessAuthorizerFactory`, `ColumnAccessChecker`, `DrillCalciteCatalogReader` |
+
+## 2. Resource Model
+
+Ranger policies for Drill use a **four-level resource hierarchy**:
+
+```
+datasource → schema → table → column
+```
+
+| Level | Ranger resource key | Example | Notes |
+| ---------- | ------------------- | ------------------- | ------------------------------------- |
+| datasource | `datasource` | `mysql` | Drill storage plugin name |
+| schema | `schema` | `shf` | Schema path WITHOUT datasource prefix |
+| table | `table` | `orders` | Table name |
+| column | `column` | `id`, `amount`, `*` | `*` matches all columns |
+
+**Critical conventions**:
+
+* Resource keys must be **lowercase** (`datasource`, not `DATASOURCE`). Ranger
+ validates names against `[a-z_-]` only (error code 2022).
+
+* The `schema` value must NOT include the datasource prefix. Use `shf`, not
+ `mysql.shf`.
+
+* Access type name in the service-def must exactly match what the code sends —
+ both uppercase `SELECT`.
+
+### 2.1 DDL Authorization (CREATE / DROP)
+
+Besides the table- and column-level `SELECT` checks issued during SQL
+validation (in `DrillCalciteCatalogReader` / `ColumnAccessChecker`), the
+engine checks CREATE/DROP privileges on the **target object** of DDL
+statements, in the DDL handlers themselves (`DdlAccessChecker`):
+
+| Statement | AccessType checked | Resource checked |
+| ----------------------------------- | ------------------ | ----------------- |
+| `CREATE TABLE ... AS SELECT` (CTAS) | `CREATE` | the new table |
+| `CREATE VIEW ... AS SELECT` | `CREATE` | the new view |
+| `DROP TABLE` | `DROP` | the dropped table |
+| `DROP VIEW` | `DROP` | the dropped view |
+
+Semantics:
+
+* **Two-layer checks for CTAS / CREATE VIEW**: the query part is still subject
+ to the `SELECT` checks above (source tables and columns), and the target
+ object additionally requires `CREATE`. A statement is executed only when
+ both checks pass.
+
+* **`CREATE VIEW OR REPLACE`** is treated as a single CREATE operation — no
+ extra DROP privilege is required.
+
+* **Temporary tables** (`CREATE TEMPORARY TABLE`, dropping a temporary table)
+ are session-scoped objects (UUID names, invisible to other users) and
+ bypass authorization.
+
+* **Authorization before existence checks**: the permission check runs before
+ "table not found" style errors, so an unauthorized user cannot probe object
+ existence through differing error messages.
+
+* **Denial surfaces as a** **`PERMISSION ERROR`** (`UserException.permissionError`),
+ exactly like a denied SELECT.
+
+* Resource mapping reuses `TableAccessResource.resolve` — the same
+ datasource/schema/table triple the SELECT checks address. For objects in
+ schema-less locations (e.g. `dfs` root) the default schema `default` is
+ synthesized (`datasource=dfs, schema=default`).
+
+A sample policy allowing all DDL creates in a schema (table-level
+`SELF_OR_DESCENDANTS` matching, so `table=*` covers every table):
+
+| Field | Value |
+| ----------- | -------- |
+| datasource | `dfs` |
+| schema | `tmp` |
+| table | `*` |
+| access type | `CREATE` |
+| user/group | (user) |
+
+> **Upgrade note (behavior change)**: once authorization is enabled, users
+> without a `CREATE` policy are now denied CTAS / CREATE VIEW (previously
+> allowed when only SELECT was checked). Same for `DROP`. Ranger denies by
+> default — plan policies accordingly when enabling.
+
+## 3. Configuration
+
+### 3.1 Drillbit side (`drill-module.conf`)
+
+```hocon
+drill.exec.security.ranger: {
+ enabled: true,
+ service.name: "drill",
+ impl: "org.apache.drill.exec.security.ranger.RangerAccessAuthorizer"
+}
+```
+
+| Key | Default | Description |
+| ----------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------- |
+| `drill.exec.security.ranger.enabled` | `false` | Master switch. `false` → `AllowAllAccessAuthorizer` (fail-open) |
+| `drill.exec.security.ranger.service.name` | `"drill"` | Ranger service name registered in Ranger Admin |
+| `drill.exec.security.ranger.impl` | `org.apache.drill.exec.security.ranger.RangerAccessAuthorizer` | `AccessAuthorizer` implementation class |
+
+### 3.2 Ranger Admin side
+
+* `drill.connection.url` — Drill REST API URL, e.g. `http://drillbit-host:8047`.
+ Bare `host:port` is normalized to `http://host:port`.
+
+* `username` / `password` — Drill user for `validateConfig` and
+ `lookupResource` REST calls (HTTP Basic auth).
+
+### 3.3 Deployment Steps
+
+After building the distribution, two deployment actions are required to make
+Ranger Admin recognize Drill as an authorization provider.
+
+#### Step 1: Update Ranger config files in Drill
+
+Copy the Ranger configuration files into Drill's `conf/` directory and edit
+them to match your environment.
+
+```bash
+DRILL_HOME=/opt/drill
+
+cp distribution/src/main/resources/ranger/ranger-drill-security.xml $DRILL_HOME/conf/
+cp distribution/src/main/resources/ranger/ranger-drill-audit.xml $DRILL_HOME/conf/
+```
+
+Then edit `$DRILL_HOME/conf/ranger-drill-security.xml`:
+
+| Property | Value to set |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| `ranger.plugin.drill.policy.rest.url` | `http://:6080` |
+| `ranger.plugin.drill.service.name` | The Ranger service name (must match `drill.exec.security.ranger.service.name` in `drill-override.conf`) |
+
+
+### 3.4 Ranger policy files
+
+| File | Location | Purpose |
+| ------------------------------ | ----------------------------------------- | -------------------------------------------------------------- |
+| `ranger-drill-security.xml` | `distribution/src/main/resources/ranger/` | Ranger plugin config (policy cache dir, polling interval) |
+| `ranger-drill-audit.xml` | `distribution/src/main/resources/ranger/` | Audit sink config (HDFS, Solr, etc.) |
+
+### 3.5 Audit Log Configuration
+
+By default, Ranger audit records are written to the **Drillbit log** via log4j.
+This is the simplest setup and requires no external dependencies. The default
+values in `ranger-drill-audit.xml` are:
+
+| Property | Default | Description |
+| -------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------- |
+| `xasecure.audit.is.enabled` | `true` | **Master switch.** Must be `true` for any audit destination to work. |
+| `xasecure.audit.log4j.is.enabled` | `true` | Audit to log4j (Drillbit log). **Enabled by default.** |
+| `xasecure.audit.solr.is.enabled` | `false` | Audit to a Solr collection. Disabled by default. |
+| `xasecure.audit.solr.url` | `http://ranger-admin-host:6083/solr/ranger_audits` | Solr endpoint (used only when `solr.is.enabled=true`). |
+| `xasecure.audit.hdfs.is.enabled` | `false` | Audit to HDFS. Disabled by default. |
+| `xasecure.audit.hdfs.config.directory` | `hdfs://namenode:8020/ranger/audit` | HDFS audit directory (used only when `hdfs.is.enabled=true`). |
+| `xasecure.audit.hdfs.config.file` | `/etc/hadoop/conf/core-site.xml` | Hadoop config file for HDFS client (used only when `hdfs.is.enabled=true`). |
+
+> **Property name caveat:** The property names above are verified from
+> `AuditProviderFactory` bytecode in `ranger-audit-core-2.8.0.jar`. The older
+> names `xasecure.audit.is.audit.to.{log4j,solr,hdfs}` are **not** read by
+> `AuditProviderFactory` and have no effect.
+
+When `xasecure.audit.log4j.is.enabled=true`, `AuditProviderFactory` loads
+`org.apache.ranger.audit.provider.Log4jAuditProvider` (from
+`ranger-audit-dest-log4j` jar) via `Class.forName()`. That class logs audit
+events through an SLF4J logger named
+`xaaudit.org.apache.ranger.audit.provider.Log4jAuditProvider`
+(the prefix `xaaudit.` is prepended to the class name in the static
+initializer).
+
+**Important:** For audit records to reach `drillbit.log`, a logger entry for
+this logger name must be present in `logback.xml`. The shipped
+`distribution/src/main/resources/logback.xml` already includes this entry:
+
+```xml
+
+
+
+```
+
+Without this entry, audit events (logged at INFO) fall through to the root
+logger (`error` level, STDOUT only) and are silently dropped.
+
+#### Verifying audit output in drillbit.log
+
+1. **Ranger Admin side** — create a policy that either allows or denies the
+ test user access to a table (e.g. `mysql.shf.orders`). Make sure the policy
+ is saved and the Drillbit has pulled it (default poll interval is 30 s).
+
+2. **Drill side** — run a query that triggers an authorization decision:
+
+ ```sql
+ SELECT id FROM mysql.shf.orders;
+ ```
+
+3. **Check drillbit.log** — look for audit entries from the
+ `xaaudit.org.apache.ranger.audit.provider.Log4jAuditProvider` logger:
+
+ ```bash
+ grep -i "xaaudit\|ranger\|audit\|access" $DRILL_HOME/log/drillbit.log | tail -20
+ ```
+
+ A typical audit log line looks like:
+
+ ```
+ 2026-08-11 10:30:45,123 [...] INFO xaaudit.org.apache.ranger.audit.provider.Log4jAuditProvider -
+ accessType=SELECT resource=mysql.shf.orders reqUser=alice ...
+ action=accessAllowed result=1
+ ```
+
+ For a denied query, `action=accessDenied` / `result=0` is logged instead.
+ If nothing appears, verify:
+
+ * `ranger-audit-dest-log4j-2.8.0.jar` is present in `$DRILL_HOME/jars/3rdparty/`
+
+ * `xasecure.audit.is.enabled=true` and `xasecure.audit.log4j.is.enabled=true`
+ in `$DRILL_HOME/conf/ranger/ranger-drill-audit.xml`
+
+ * The `logback.xml` entry above is present
+
+ * The Drillbit was restarted after editing configuration
+
+#### Switching the audit destination
+
+To send audit records to **Solr** instead of (or in addition to) the Drillbit
+log, edit `$DRILL_HOME/conf/ranger/ranger-drill-audit.xml` after deployment:
+
+```xml
+
+ xasecure.audit.solr.is.enabled
+ true
+
+
+ xasecure.audit.solr.url
+ http://your-ranger-admin-host:6083/solr/ranger_audits
+
+```
+
+To send audit records to **HDFS**:
+
+```xml
+
+ xasecure.audit.hdfs.is.enabled
+ true
+
+
+ xasecure.audit.hdfs.config.directory
+ hdfs://your-namenode:8020/ranger/audit
+
+
+ xasecure.audit.hdfs.config.file
+ /etc/hadoop/conf/core-site.xml
+
+```
+
+Multiple sinks can be enabled simultaneously — for example, keep `log4j=true`
+as a local fallback while also forwarding to Solr for centralized search. After
+changing the file, restart the Drillbit for the new settings to take effect.
+
+## 4. Authorization Policy Test Cases
+
+The following test cases document the expected authorization behavior with the
+sample policies below. All SQL runs against tables `mysql.shf.orders` and
+`mysql.shf.users`.
+
+### 4.1 Sample Ranger Policies
+
+**Policy A — users table, all columns**
+
+| Field | Value |
+| ----------- | ----------------- |
+| datasource | `mysql` |
+| schema | `shf` |
+| table | `users` |
+| column | `*` |
+| access type | `SELECT` |
+| user/group | (authorized user) |
+
+**Policy B — orders table, specific columns only**
+
+| Field | Value |
+| ----------- | ----------------- |
+| datasource | `mysql` |
+| schema | `shf` |
+| table | `orders` |
+| column | `id`, `amount` |
+| access type | `SELECT` |
+| user/group | (authorized user) |
+
+**Policy C — DDL creates in the shf schema**
+
+| Field | Value |
+| ----------- |-------------------|
+| datasource | `mysql`,`dfs` |
+| schema | `shf` ,`test` |
+| table | `*` |
+| access type | `CREATE` |
+| user/group | (authorized user) |
+
+Under these policies, the `orders.user_id` and `orders.order_date` columns are
+NOT authorized. The `users` table allows all columns via `*`. Policy C grants
+CTAS / CREATE VIEW (but not DROP) anywhere in `dfs.test`.
+
+### 4.2 Test Cases
+
+| # | SQL | Expected | Why |
+| -- |-----------------------------------------------------------------------------------------------------------------------| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| 1 | `SELECT id, amount FROM orders` | **PASS** | `id`, `amount` both in Policy B |
+| 2 | `SELECT * FROM orders` | **DENY** | `*` expands to all columns including `user_id`, which is not in Policy B |
+| 3 | `SELECT sum(id) FROM orders` | **PASS** | Aggregate on authorized column `id` |
+| 4 | `SELECT id FROM orders WHERE user_id > 1` | **DENY** | `user_id` in WHERE clause is checked (LogicalFilter condition traced) |
+| 5 | `SELECT sum(user_id) FROM orders` | **DENY** | `user_id` not in Policy B |
+| 6 | `SELECT o.id, u.name, o.amount, o.order_date FROM orders o INNER JOIN users u ON o.user_id = u.id` | **DENY** | `o.user_id` appears in JOIN condition; `o.order_date` not in Policy B |
+| 7 | `SELECT o.id, u.name, o.amount FROM orders o INNER JOIN users u ON o.id = u.id` | **PASS** | All referenced columns authorized: `o.id`, `u.name` (via `*`), `o.amount` |
+| 8 | `SELECT u.*, o.amount FROM orders o JOIN users u ON o.id = u.id WHERE o.amount > (SELECT AVG(amount) FROM orders)` | **PASS** | `u.*` authorized via Policy A; `o.amount` and subquery `amount` in Policy B |
+| 9 | `SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id WHERE o.amount > (SELECT AVG(amount) FROM orders)` | **DENY** | `o.*` expands to `user_id` (not authorized); `o.user_id` in JOIN condition |
+| 10 | `SELECT o.id, u.name FROM orders o JOIN users u ON o.id = u.id WHERE o.amount > (SELECT AVG(amount) FROM orders)` | **PASS** | All columns authorized; subquery only references `amount` |
+| 11 | `SELECT o.id, u.name FROM orders o JOIN users u ON o.id = u.id WHERE o.amount > (SELECT sum(user_id) FROM orders)` | **DENY** | Scalar subquery references `user_id` (not authorized). **Requires RexSubQuery handling.** |
+| 12 | `SELECT name FROM users WHERE id IN (SELECT DISTINCT user_id FROM orders)` | **DENY** | IN-subquery references `user_id` (not authorized). **Requires RexSubQuery handling.** |
+| 13 | `SELECT * FROM users u WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id)` | **DENY** | EXISTS-subquery references `o.user_id` (not authorized). **Requires RexSubQuery handling.** |
+| 14 | `WITH t AS (SELECT * FROM mysql.shf.orders) SELECT * FROM t` | **DENY** | CTE body `SELECT *` expands to all columns including `order_date` (not in Policy B). CTE is inlined before `ColumnAccessChecker` runs, so the underlying `orders` TableScan is checked. |
+| 15 | `WITH t AS (SELECT id FROM mysql.shf.orders) SELECT * FROM t` | **PASS** | CTE body only selects `id` (in Policy B); outer `SELECT *` from CTE resolves to the same authorized column. |
+| 16 | `WITH t AS (SELECT id, order_date FROM mysql.shf.orders) SELECT order_date FROM t` | **DENY** | CTE body selects `order_date` (not in Policy B). Column check traces back to the underlying `orders` TableScan after CTE inlining. |
+| 17 | `WITH t AS (SELECT id, order_date FROM mysql.shf.orders) SELECT id FROM t` | **DENY** | Even though the outer query only projects `id`, the CTE body references `order_date` (not in Policy B). After inlining, the `orders` TableScan has both columns referenced, so the check fails on `order_date`. |
+| 18 | `WITH a AS (SELECT id FROM mysql.shf.orders), b AS (SELECT id FROM mysql.shf.users) SELECT * FROM b` | **PASS** | Multiple CTEs: `a` selects `id` from `orders` (Policy B); `b` selects `id` from `users` (Policy A via `*`). Outer query selects from `b` only. All referenced columns authorized. |
+| 19 | `SELECT o.order_date FROM mysql.shf.orders o` | **DENY** | Table alias is transparent: `o.order_date` resolves to `orders.order_date` (not in Policy B). `RexInputRef` indexes point to row-type positions, and `getColumnOrigins` traces through to the underlying `TableScan` regardless of alias. |
+| 20 | `SELECT a.id FROM mysql.shf.users a JOIN mysql.shf.orders b ON a.id = b.id` | **PASS** | Join with aliases: `a.id` resolves to `users.id` (Policy A via `*`); `b.id` resolves to `orders.id` (Policy B). Join condition columns also authorized. |
+| 21 | `SELECT o.id FROM mysql.shf.orders o` | **PASS** | Simple table alias on an authorized column; `o.id` resolves to `orders.id` (Policy B). |
+| 22 | `SELECT o.id FROM mysql.shf.orders o WHERE o.amount = '150.0'` | **PASS** | Alias in WHERE clause on an authorized column; `o.amount` resolves to `orders.amount` (Policy B). |
+| 23 | `SELECT o.id FROM mysql.shf.orders o WHERE EXISTS (SELECT 1 FROM mysql.shf.users u WHERE u.id = o.user_id)` | **DENY** | Correlated subquery: the outer-column reference `o.user_id` (represented as a `RexFieldAccess` over a `RexCorrelVariable`, not a `RexInputRef`) resolves to `orders.user_id`, which is not in Policy B. The outer query is NOT `SELECT *`, so nothing else covers `orders.user_id` — the denial comes solely from the correlated reference. **Requires** **`RexFieldAccess`/`RexCorrelVariable`** **handling**; without it this column is silently bypassed. (Case 13 does not catch this because its outer `SELECT *` over `users` masks the bypass and its correlated column `u.id` is authorized via Policy A.) |
+| 24 | `SELECT o.id FROM mysql.shf.orders o WHERE EXISTS (SELECT 1 FROM mysql.shf.users u WHERE u.id = o.id)` | **PASS** | Same shape as case 23, but the correlated reference `o.id` resolves to `orders.id` (Policy B). Confirms that correlated references to authorized outer columns are still allowed. |
+| 25 | `SELECT SUM(o.amount) FILTER (WHERE o.user_id > 0) FROM mysql.shf.orders o` | **DENY** | Aggregate `FILTER (WHERE ...)` clause: `o.user_id` is not in Policy B. In the Rel tree, Calcite lowers the FILTER predicate into a boolean column beneath the `LogicalAggregate`, and the `AggregateCall.filterArg` ordinal points at that column. `visit(LogicalAggregate)` traces `filterArg` explicitly so the unauthorized `orders.user_id` is caught even without relying on an implicit Project beneath. **Without the** **`filterArg`** **trace this column is silently bypassed.** |
+| 26 | `SELECT SUM(o.amount) FILTER (WHERE o.id > 0) FROM mysql.shf.orders o` | **PASS** | Same shape as case 25, but the FILTER clause references `o.id` (Policy B). Confirms that aggregate call arguments and FILTER columns that are authorized are still allowed. **Note:** The `FILTER (WHERE ...)` clause is SQL:2008 standard but not supported by MySQL; the permission check passes but query execution will fail on MySQL storage. Use PostgreSQL or a Drill-native table for end-to-end execution, or use `SELECT SUM(CASE WHEN id > 0 THEN amount END) FROM ...` as a MySQL-compatible alternative (though `CASE` goes through `argList`, not `filterArg`). |
+| 27 | `CREATE TABLE mysql.shf.new_orders AS SELECT id, amount FROM mysql.shf.orders` | **PASS** | Target table `new_orders` covered by Policy C (`CREATE` on `mysql.shf.*`); query part references only `id`, `amount` (Policy B). Both layers pass. |
+| 28 | `CREATE TABLE mysql.shf.new_orders AS SELECT * FROM mysql.shf.orders` | **DENY** | `CREATE` on the target is granted (Policy C), but the query part's `SELECT *` expands to unauthorized columns (`user_id`, `order_date` — not in Policy B). The source SELECT check rejects the statement before the target CREATE check matters. |
+| 29 | `DROP TABLE mysql.shf.orders` | **DENY** | No policy grants `DROP` on `mysql.shf.*` (Policy C is CREATE-only). The DDL check in `DropTableHandler` denies with a PERMISSION ERROR before the existence check. |
+| 30 | `use dfs.test; CREATE VIEW v_orders AS SELECT id FROM mysql.shf.orders; DROP VIEW v_orders` | **PASS / DENY** | `CREATE VIEW` passes (Policy C, query part only references `id` in Policy B). `DROP VIEW` is denied: no `DROP` policy. `OR REPLACE` on the view would still be a single CREATE check (PASS). Temporary tables bypass both checks entirely. |
+
diff --git a/exec/java-exec/pom.xml b/exec/java-exec/pom.xml
index fe2c229a9a0..92dda18d79f 100644
--- a/exec/java-exec/pom.xml
+++ b/exec/java-exec/pom.xml
@@ -318,6 +318,17 @@
drill-logical
${project.version}
+
+
+ org.apache.drill
+ drill-security-spi
+ ${project.version}
+
org.apache.drill.exec
drill-rpc
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java b/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java
index b511daa9c2f..febcc64ded7 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/ExecConstants.java
@@ -313,6 +313,10 @@ private ExecConstants() {
public static final String BIT_ENCRYPTION_SASL_ENABLED = "drill.exec.security.bit.encryption.sasl.enabled";
public static final String BIT_ENCRYPTION_SASL_MAX_WRAPPED_SIZE = "drill.exec.security.bit.encryption.sasl.max_wrapped_size";
+ // Access authorization (generic; Ranger is one provider selected via 'name')
+ public static final String AUTHORIZER_ENABLED = "drill.exec.security.authorizer.enabled";
+ public static final String AUTHORIZER_NAME = "drill.exec.security.authorizer.name";
+
/** Size of JDBC batch queue (in batches) above which throttling begins. */
public static final String JDBC_BATCH_QUEUE_THROTTLING_THRESHOLD =
"drill.jdbc.batch_queue_throttling_threshold";
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/ColumnAccessChecker.java b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/ColumnAccessChecker.java
new file mode 100644
index 00000000000..3fd5b674e46
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/ColumnAccessChecker.java
@@ -0,0 +1,511 @@
+/*
+ * 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.drill.exec.planner.sql.conversion;
+
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.rel.RelFieldCollation;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelShuttleImpl;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.logical.LogicalSort;
+import org.apache.calcite.rel.metadata.RelColumnOrigin;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rex.RexCorrelVariable;
+import org.apache.calcite.rex.RexFieldAccess;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexSubQuery;
+import org.apache.calcite.rex.RexVisitor;
+import org.apache.calcite.rex.RexVisitorImpl;
+import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.common.exceptions.UserException;
+import org.apache.drill.exec.rpc.user.UserSession;
+import org.apache.drill.exec.security.AccessAuthorizerManager;
+import org.apache.drill.exec.security.TableAccessResource;
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Visitor that traverses a RelNode tree and enforces column-level SELECT authorization
+ * via the configured {@link AccessAuthorizer} (Ranger by default).
+ *
+ * Design: For every RelNode that carries {@link RexNode} expressions
+ * (Project, Filter, Join, Aggregate, Sort), the visitor collects all {@link RexInputRef}s
+ * and uses Calcite's {@link RelMetadataQuery#getColumnOrigins(RelNode, int)} to trace each
+ * referenced column back to its originating {@link TableScan} column index. This correctly
+ * handles multi-hop projections, filters, joins, and aggregations.
+ *
+ * For a {@link TableScan} that has NO traced column references (e.g.
+ * {@code SELECT * FROM t} with no intervening Project), ALL columns of that table
+ * are checked.
+ *
+ * Correlated subqueries are handled as well: a reference to an outer query
+ * column from inside a correlated subquery is represented by Calcite as a
+ * {@link RexFieldAccess} over a {@link RexCorrelVariable} (e.g. {@code $cor0.id}),
+ * not as a {@link RexInputRef}. Each correlation variable carries a unique
+ * name {@code $corN} assigned by {@code SqlToRelConverter}; we keep an exact
+ * map from variable name to the enclosing {@link RelNode} row scope it refers
+ * to. The map entry is populated the FIRST time we see a given {@code $corN}
+ * during Rex traversal: we use {@code putIfAbsent(name, enclosingScope)} so
+ * that subsequent encounters (including cross-nested skip-level references
+ * from deeper subqueries to an outer {@code $cor0}) never overwrite the
+ * original, correct binding. This avoids the ambiguity of row-type-based
+ * heuristics: multiple enclosing scopes with identical schemas no longer
+ * cause false positives or missed detections, and no scope stack is needed —
+ * a single field tracks the innermost enclosing scope and is saved/restored
+ * around each subquery entry via try/finally.
+ *
+ * System schemas (INFORMATION_SCHEMA, sys) are bypassed inside the authorizer
+ * implementation. When authorization is disabled, the visitor is a no-op (fail-open).
+ */
+class ColumnAccessChecker extends RelShuttleImpl {
+
+ private static final Logger logger = LoggerFactory.getLogger(ColumnAccessChecker.class);
+
+ private final UserSession session;
+ private final DrillConfig drillConfig;
+ private final RelMetadataQuery mq;
+
+ // Records each table's referenced column indices. Uses IdentityHashMap because
+ // RelOptTable equals/hashCode may be expensive or not identity-based.
+ private final Map> tableToReferencedCols = new IdentityHashMap<>();
+
+ // The innermost enclosing row scope for correlated references. When we are
+ // about to descend into a RexSubQuery's Rel tree, we save the previous value
+ // and set this field to the outer-query inputNode that the subquery's
+ // $cor* variables are relative to; the saved value is restored in a finally
+ // block. Because variable → scope registration uses putIfAbsent in the Rex
+ // collector, a single field is sufficient even for deep nesting: skip-level
+ // references to an outer $corN reuse the binding that was established the
+ // first time that variable was seen (at a shallower enclosing scope).
+ private RelNode enclosingScope;
+
+ // Exact, name-keyed scope mapping: $cor0 → outer row scope, $cor1 → outer
+ // row scope, etc. Populated with putIfAbsent during Rex traversal so the
+ // first occurrence of each $corN establishes a permanent binding. Lookup is
+ // pure get(); a miss falls back (safely, in the over-approximate direction)
+ // to enclosingScope and emits a warning so unusual plans remain diagnosable.
+ private final Map correlationScopeByVar = new HashMap<>();
+
+ ColumnAccessChecker(UserSession session, DrillConfig drillConfig, RelMetadataQuery mq) {
+ this.session = session;
+ this.drillConfig = drillConfig;
+ this.mq = mq;
+ }
+
+ /**
+ * Entry point: traverse the tree and enforce column-level access.
+ */
+ void check(RelNode root) {
+ // Also trace the root node's output columns (covers bare TableScan root or
+ // top-level Project output).
+ traceOutputColumns(root);
+
+ // Walk the tree to collect RexInputRef origins from all expression-bearing nodes.
+ root.accept(this);
+ }
+
+ // ------------------------------------------------------------------
+ // RelShuttle overrides — collect RexInputRefs from expression-bearing nodes
+ // ------------------------------------------------------------------
+
+ @Override
+ public RelNode visit(LogicalProject project) {
+ collectRefs(project.getProjects(), project.getInput());
+ return super.visit(project);
+ }
+
+ @Override
+ public RelNode visit(LogicalFilter filter) {
+ if (filter.getCondition() != null) {
+ analyzeRex(filter.getCondition(), filter.getInput(), -1, null);
+ }
+ return super.visit(filter);
+ }
+
+ @Override
+ public RelNode visit(LogicalJoin join) {
+ if (join.getCondition() != null) {
+ int leftCount = join.getLeft().getRowType().getFieldCount();
+ analyzeRex(join.getCondition(), join.getRight(), leftCount, join.getLeft());
+ }
+ return super.visit(join);
+ }
+
+ @Override
+ public RelNode visit(LogicalAggregate aggregate) {
+ for (int i : aggregate.getGroupSet()) {
+ traceColumnOrigin(aggregate.getInput(), i);
+ }
+ // AggregateCall arguments (the x in SUM(x)), FILTER (WHERE ...) columns,
+ // and WITHIN GROUP collation columns are not covered by getGroupSet().
+ // Trace them explicitly rather than relying on an implicit Project beneath
+ // the aggregate (plan shape can change across Calcite upgrades).
+ for (AggregateCall call : aggregate.getAggCallList()) {
+ for (int arg : call.getArgList()) {
+ traceColumnOrigin(aggregate.getInput(), arg);
+ }
+ if (call.filterArg >= 0) {
+ traceColumnOrigin(aggregate.getInput(), call.filterArg);
+ }
+ if (call.getCollation() != null) {
+ for (RelFieldCollation fc : call.getCollation().getFieldCollations()) {
+ traceColumnOrigin(aggregate.getInput(), fc.getFieldIndex());
+ }
+ }
+ }
+ return super.visit(aggregate);
+ }
+
+ @Override
+ public RelNode visit(LogicalSort sort) {
+ if (sort.getCollation() != null) {
+ sort.getCollation().getFieldCollations().forEach(fc ->
+ traceColumnOrigin(sort.getInput(), fc.getFieldIndex()));
+ }
+ // LIMIT/OFFSET are RexNode expressions that may carry RexSubQuery or
+ // RexInputRef (e.g. LIMIT (SELECT MAX(amount) FROM ...)). analyzeRex
+ // descends into them; without it, subqueries reachable only from Sort
+ // would never be column-checked.
+ if (sort.fetch != null) {
+ analyzeRex(sort.fetch, sort.getInput(), -1, null);
+ }
+ if (sort.offset != null) {
+ analyzeRex(sort.offset, sort.getInput(), -1, null);
+ }
+ return super.visit(sort);
+ }
+
+ @Override
+ public RelNode visit(TableScan scan) {
+ RelOptTable table = scan.getTable();
+
+ // Determine which columns to check: traced columns, or ALL if none were traced
+ // (SELECT * FROM t case).
+ Set referencedColIndices = tableToReferencedCols.get(table);
+ List allColumnNames = scan.getRowType().getFieldNames();
+
+ Set columnsToCheck;
+ if (referencedColIndices == null || referencedColIndices.isEmpty()) {
+ // SELECT * — check all columns
+ columnsToCheck = new HashSet<>(allColumnNames);
+ } else {
+ columnsToCheck = new HashSet<>();
+ for (int idx : referencedColIndices) {
+ if (idx >= 0 && idx < allColumnNames.size()) {
+ columnsToCheck.add(allColumnNames.get(idx));
+ }
+ }
+ }
+
+ if (columnsToCheck.isEmpty()) {
+ return scan;
+ }
+
+ enforceColumnAccess(table, columnsToCheck);
+ return scan;
+ }
+
+ /**
+ * Traces the output columns of a RelNode back to their table-scan origins.
+ */
+ private void traceOutputColumns(RelNode node) {
+ if (node == null) {
+ return;
+ }
+ int fieldCount = node.getRowType().getFieldCount();
+ for (int i = 0; i < fieldCount; i++) {
+ traceColumnOrigin(node, i);
+ }
+ }
+
+ /**
+ * Traces a single output column of {@code node} at index {@code columnIndex} back to
+ * table-scan origins, recording them in {@link #tableToReferencedCols}.
+ */
+ private void traceColumnOrigin(RelNode node, int columnIndex) {
+ if (node == null || mq == null) {
+ return;
+ }
+ Set origins;
+ try {
+ origins = mq.getColumnOrigins(node, columnIndex);
+ } catch (Exception e) {
+ logger.debug("getColumnOrigins failed for {} column {}", node, columnIndex, e);
+ return;
+ }
+ if (origins == null) {
+ return;
+ }
+ for (RelColumnOrigin origin : origins) {
+ RelOptTable originTable = origin.getOriginTable();
+ if (originTable != null) {
+ // Record origins for ALL table types (DrillTable, JdbcTable, etc.).
+ // Previously this only recorded DrillTable origins, which caused
+ // JDBC storage plugin tables (JdbcTable) to be skipped entirely.
+ tableToReferencedCols
+ .computeIfAbsent(originTable, k -> new HashSet<>())
+ .add(origin.getOriginColumnOrdinal());
+ }
+ }
+ }
+
+ /**
+ * Collects RexInputRefs from a list of RexNodes and traces each to its
+ * table-scan origin via the input node's metadata. Also processes any
+ * {@link RexSubQuery} found in the expressions (scalar/IN/EXISTS subqueries)
+ * so that column references inside subqueries are authorized.
+ */
+ private void collectRefs(List rexNodes, RelNode inputNode) {
+ if (rexNodes == null || inputNode == null) {
+ return;
+ }
+ for (RexNode rex : rexNodes) {
+ analyzeRex(rex, inputNode, -1, null);
+ }
+ }
+
+ /**
+ * Analyzes a {@link RexNode} expression, collecting {@link RexInputRef}s and
+ * {@link RexSubQuery}s, tracing each input ref to its table-scan origin and
+ * recursively visiting each subquery's {@link RelNode} tree.
+ *
+ * @param rex the expression to analyze
+ * @param inputNode the input RelNode that RexInputRefs resolve against
+ * @param leftCount if {@code >= 0}, indicates a join condition: refs with
+ * index {@code < leftCount} resolve against {@code leftInput},
+ * others resolve against {@code inputNode} (the right input)
+ * with offset {@code leftCount}. If {@code < 0}, all refs
+ * resolve against {@code inputNode}.
+ * @param leftInput the left input of a join, or {@code null} when
+ * {@code leftCount < 0}.
+ */
+ private void analyzeRex(RexNode rex, RelNode inputNode, int leftCount, RelNode leftInput) {
+ if (rex == null) {
+ return;
+ }
+ Set refs = new HashSet<>();
+ List subQueries = new ArrayList<>();
+ List correlAccesses = new ArrayList<>();
+ rex.accept(new RexRefCollector(refs, subQueries, correlAccesses));
+ for (int refIndex : refs) {
+ if (leftCount >= 0 && refIndex < leftCount) {
+ traceColumnOrigin(leftInput, refIndex);
+ } else if (leftCount >= 0) {
+ traceColumnOrigin(inputNode, refIndex - leftCount);
+ } else {
+ traceColumnOrigin(inputNode, refIndex);
+ }
+ }
+ // Resolve correlated outer-column references. A RexFieldAccess over a
+ // RexCorrelVariable (e.g. $cor0.id inside a correlated subquery) always
+ // refers to an ENCLOSING subquery's row scope, never to inputNode itself
+ // (a reference to inputNode would be a plain RexInputRef).
+ //
+ // Registration happens inside RexRefCollector.visitFieldAccess with
+ // putIfAbsent(varName, enclosingScope), so by the time we reach here the
+ // scope for every $corN we just collected has been bound if possible.
+ // Lookup is a direct map get(); the uncommon case of an unbound variable
+ // falls back (safely over-approximating) to enclosingScope before giving
+ // up — tracing an extra scope is harmless and keeps us on the secure side
+ // of the check.
+ for (CorrelFieldAccess cfa : correlAccesses) {
+ RelNode scope = correlationScopeByVar.get(cfa.varName);
+ if (scope != null) {
+ traceColumnOrigin(scope, cfa.fieldIndex);
+ continue;
+ }
+ if (enclosingScope != null) {
+ logger.debug("Correlated reference '{}' was not pre-bound; "
+ + "falling back to current enclosingScope for a safe over-approximation.",
+ cfa.varName);
+ traceColumnOrigin(enclosingScope, cfa.fieldIndex);
+ } else {
+ logger.warn("Unresolved correlated column reference '{}' (field index {}): "
+ + "no enclosing correlation scope could be determined. Column-level "
+ + "authorization may be incomplete for this reference.",
+ cfa.varName, cfa.fieldIndex);
+ }
+ }
+ for (RexSubQuery sq : subQueries) {
+ // Trace the subquery's output columns to their table-scan origins.
+ // For scalar subqueries (e.g. SELECT sum(user_id) FROM t), this traces
+ // the aggregate output back to the underlying table column.
+ traceOutputColumns(sq.rel);
+ // The subquery's $cor* variables reference the rows produced by
+ // inputNode (the input of the node whose expression contained this
+ // RexSubQuery). Make inputNode the active enclosing scope while we
+ // descend into sq.rel; save/restore the previous value so nesting and
+ // skip-level references behave correctly. Registration of new $corN
+ // names in correlationScopeByVar uses putIfAbsent, so a variable whose
+ // binding was established at a shallower scope is never overwritten.
+ RelNode prevEnclosing = enclosingScope;
+ enclosingScope = inputNode;
+ try {
+ // Recursively visit the subquery's RelNode tree so that RexInputRefs
+ // and correlated references inside the subquery are also collected
+ // and traced.
+ sq.rel.accept(this);
+ } finally {
+ enclosingScope = prevEnclosing;
+ }
+ }
+ }
+
+ /**
+ * Enforces column-level access for the given table and column set.
+ */
+ private void enforceColumnAccess(RelOptTable table, Set columns) {
+ AccessAuthorizer authorizer = AccessAuthorizerManager.getAuthorizer(drillConfig);
+ // When authorization is disabled the manager returns the NoOp authorizer,
+ // which allows all access — no enabled/disabled branching needed here.
+
+ // Resolve datasource / schema / table from the qualified name via the
+ // shared resolver, so column-level checks address exactly the same
+ // resource as the table-level checks in DrillCalciteCatalogReader. This
+ // works for ALL table types (DrillTable, JdbcTable, etc.) — previously
+ // this method required a DrillTable and skipped JdbcTable, leaving JDBC
+ // storage plugin tables without column-level authorization.
+ TableAccessResource resource = TableAccessResource.resolve(table.getQualifiedName());
+ String userName = session.getCredentials().getUserName();
+
+ if (!authorizer.checkColumnAccess(UserIdentity.of(userName), resource.getDataSource(),
+ resource.getSchemaPath(), resource.getTable(), columns, AccessType.SELECT)) {
+ throw UserException.permissionError()
+ .message("Access denied: user '%s' lacks SELECT privilege on one or more columns " +
+ "(%s) of table %s", userName, columns, resource)
+ .build(logger);
+ }
+ }
+
+ /**
+ * Holder for a correlated outer-column reference captured during Rex
+ * traversal: the variable name of the {@link RexCorrelVariable}
+ * (e.g. {@code "$cor0"}, used to do an exact lookup of the enclosing row
+ * scope via {@link #correlationScopeByVar}) and the index of the referenced
+ * field within that row scope.
+ */
+ private static final class CorrelFieldAccess {
+ final String varName;
+ final int fieldIndex;
+
+ CorrelFieldAccess(String varName, int fieldIndex) {
+ this.varName = varName;
+ this.fieldIndex = fieldIndex;
+ }
+ }
+
+ /**
+ * RexVisitor that collects all {@link RexInputRef} indices,
+ * {@link RexSubQuery} instances, and correlated outer-column references
+ * encountered in a {@link RexNode} tree. A correlated reference to an outer
+ * query column appears as a {@link RexFieldAccess} over a
+ * {@link RexCorrelVariable} (e.g. {@code $cor0.id}); such references are not
+ * {@link RexInputRef}s and would be silently skipped by a plain
+ * {@code RexInputRef}-only visitor, bypassing column-level authorization for
+ * the referenced outer column.
+ *
+ * This collector is non-static so it can eagerly register each {@code
+ * $corN} in {@link #correlationScopeByVar} at the exact moment we first
+ * encounter it during Rex traversal. Registration is
+ * {@code putIfAbsent(name, enclosingScope)}, which guarantees we never
+ * overwrite a variable that was already bound at a shallower nesting level
+ * (which is exactly how skip-level references to outer variables stay
+ * correctly bound).
+ *
+ *
{@link RexSubQuery#accept(RexVisitor)} dispatches to
+ * {@link #visitSubQuery(RexSubQuery)} (not {@code visitCall}), so a plain
+ * {@code RexInputRef}-only visitor silently skips over subqueries. This
+ * collector overrides {@code visitSubQuery} to capture the subquery and then
+ * continues traversing its operands so that nested {@link RexInputRef}s
+ * (e.g. the left side of {@code x IN (SELECT ...)}) and nested subqueries
+ * are also collected.
+ */
+ private final class RexRefCollector extends RexVisitorImpl {
+ private final Set refs;
+ private final List subQueries;
+ private final List correlAccesses;
+
+ RexRefCollector(Set refs, List subQueries,
+ List correlAccesses) {
+ super(true);
+ this.refs = refs;
+ this.subQueries = subQueries;
+ this.correlAccesses = correlAccesses;
+ }
+
+ @Override
+ public Void visitInputRef(RexInputRef ref) {
+ refs.add(ref.getIndex());
+ return null;
+ }
+
+ @Override
+ public Void visitSubQuery(RexSubQuery subQuery) {
+ subQueries.add(subQuery);
+ // Continue traversing operands (e.g. the left expression of `x IN (...)`)
+ // to collect any RexInputRefs and nested RexSubQueries within them.
+ for (RexNode operand : subQuery.getOperands()) {
+ operand.accept(this);
+ }
+ return null;
+ }
+
+ @Override
+ public Void visitFieldAccess(RexFieldAccess fieldAccess) {
+ // A RexFieldAccess over a RexCorrelVariable is how Calcite represents a
+ // correlated reference to an outer query's column (e.g. $cor0.id inside
+ // a subquery). Such references are not RexInputRefs and would otherwise
+ // be silently skipped by this collector, bypassing column-level
+ // authorization for the referenced outer column.
+ RexNode refExpr = fieldAccess.getReferenceExpr();
+ if (refExpr instanceof RexCorrelVariable) {
+ RexCorrelVariable corVar = (RexCorrelVariable) refExpr;
+ String varName = corVar.getName();
+ // Eager, idempotent registration: putIfAbsent so the first
+ // enclosing scope we saw this variable under wins, and any later
+ // occurrences (possibly from a deeper nested scope after
+ // enclosingScope has been overwritten) do not clobber the binding.
+ if (enclosingScope != null) {
+ correlationScopeByVar.putIfAbsent(varName, enclosingScope);
+ }
+ correlAccesses.add(new CorrelFieldAccess(varName,
+ fieldAccess.getField().getIndex()));
+ }
+ // Preserve default descent so that non-correlated field accesses (e.g.
+ // struct field access over a regular column) continue to be traversed
+ // exactly as before this override.
+ return super.visitFieldAccess(fieldAccess);
+ }
+ }
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/DrillCalciteCatalogReader.java b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/DrillCalciteCatalogReader.java
index 53644d4c094..2316b0fb76c 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/DrillCalciteCatalogReader.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/DrillCalciteCatalogReader.java
@@ -35,6 +35,11 @@
import org.apache.drill.exec.planner.logical.DrillTable;
import org.apache.drill.exec.planner.sql.SchemaUtilities;
import org.apache.drill.exec.rpc.user.UserSession;
+import org.apache.drill.exec.security.AccessAuthorizerManager;
+import org.apache.drill.exec.security.TableAccessResource;
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
@@ -103,14 +108,53 @@ void disallowTemporaryTables() {
public Prepare.PreparingTable getTable(List names) {
checkTemporaryTable(names);
Prepare.PreparingTable table = super.getTable(names);
- DrillTable drillTable;
- if (table != null && (drillTable = table.unwrap(DrillTable.class)) != null) {
- drillTable.setOptions(session.getOptions());
- drillTable.setTableMetadataProviderManager(tableCache.getUnchecked(DrillTableKey.of(names, drillTable)));
+ if (table != null) {
+ // Ranger SELECT authorization check for ALL table types, including
+ // JDBC storage plugin tables (JdbcTable) that are not DrillTable.
+ checkTableAccess(table);
+
+ DrillTable drillTable = table.unwrap(DrillTable.class);
+ if (drillTable != null) {
+ drillTable.setOptions(session.getOptions());
+ drillTable.setTableMetadataProviderManager(tableCache.getUnchecked(DrillTableKey.of(names, drillTable)));
+ }
}
return table;
}
+ /**
+ * Checks SELECT permission on the resolved table via the configured {@link AccessAuthorizer}
+ * (Ranger by default). No-op when authorization is disabled (fail-open). System schemas
+ * (INFORMATION_SCHEMA, sys) are bypassed inside the authorizer implementation.
+ *
+ * Extracts datasource/schema/table from the resolved qualified name via
+ * {@link TableAccessResource#resolve(List)} so it works for both DrillTable
+ * (native storage plugins) and non-DrillTable (JDBC storage plugin), and so
+ * table-level and column-level checks address exactly the same resource.
+ */
+ private void checkTableAccess(Prepare.PreparingTable table) {
+ if (!AccessAuthorizerManager.isEnabled(drillConfig)) {
+ // When authorization is disabled, skip the check entirely (matching the
+ // SqlConverter guard). This also avoids the UserSession.getCredentials()
+ // call below, which would NPE for sessions without credentials (e.g.
+ // mock sessions in planner unit tests).
+ return;
+ }
+ AccessAuthorizer authorizer = AccessAuthorizerManager.getAuthorizer(drillConfig);
+ // Use the resolved qualified name (includes default schema resolution) rather than
+ // the raw input names, which may be incomplete when the user omits the schema.
+ TableAccessResource resource = TableAccessResource.resolve(table.getQualifiedName());
+ String userName = session.getCredentials().getUserName();
+
+ if (!authorizer.checkTableAccess(UserIdentity.of(userName), resource.getDataSource(),
+ resource.getSchemaPath(), resource.getTable(), AccessType.SELECT)) {
+ throw UserException.permissionError()
+ .message("Access denied: user '%s' lacks SELECT privilege on %s",
+ userName, resource)
+ .build(logger);
+ }
+ }
+
private void checkTemporaryTable(List names) {
if (allowTemporaryTables || !needsTemporaryTableCheck(names, session.getDefaultSchemaPath(), drillConfig)) {
return;
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/SqlConverter.java b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/SqlConverter.java
index 25ed545c687..1f80fe42fc1 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/SqlConverter.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/conversion/SqlConverter.java
@@ -62,6 +62,7 @@
import org.apache.drill.exec.planner.sql.parser.impl.DrillSqlParseException;
import org.apache.drill.exec.planner.types.DrillRelDataTypeSystem;
import org.apache.drill.exec.rpc.user.UserSession;
+import org.apache.drill.exec.security.AccessAuthorizerManager;
import org.apache.drill.exec.util.ImpersonationUtil;
import org.apache.drill.exec.util.Utilities;
import org.slf4j.Logger;
@@ -245,6 +246,19 @@ public RelRoot toRel(final SqlNode validatedNode) {
RelNode project = LogicalProject.create(rel.rel, Collections.emptyList(), expressions, rel.validatedRowType);
rel = RelRoot.of(project, rel.validatedRowType, rel.kind);
}
+
+ // Column-level SELECT authorization check. Done after SqlToRelConverter has
+ // resolved all column references (so we can trace each to its TableScan)
+ // and before flattenTypes/optimization (so column references are intact).
+ //
+ // Skip the whole check when authorization is disabled: the tree walk plus
+ // a getColumnOrigins() metadata query per output column is measurable
+ // planning cost that deployments not using the feature must not pay on
+ // every query.
+ if (AccessAuthorizerManager.isEnabled(drillConfig)) {
+ new ColumnAccessChecker(session, drillConfig, cluster.getMetadataQuery()).check(rel.rel);
+ }
+
return rel.withRel(sqlToRelConverter.flattenTypes(rel.rel, true));
}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/CreateTableHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/CreateTableHandler.java
index dde067a1130..6e88a8a74cd 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/CreateTableHandler.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/CreateTableHandler.java
@@ -43,6 +43,9 @@
import org.apache.drill.exec.physical.base.PhysicalOperator;
import org.apache.drill.exec.planner.sql.DirectPlan;
import org.apache.drill.exec.rpc.user.UserSession;
+import org.apache.drill.exec.security.AccessAuthorizerManager;
+import org.apache.drill.exec.security.DdlAccessChecker;
+import org.apache.drill.exec.security.spi.AccessType;
import org.apache.drill.exec.store.StorageStrategy;
import org.apache.drill.exec.planner.logical.DrillRel;
import org.apache.drill.exec.planner.logical.DrillScreenRel;
@@ -87,6 +90,14 @@ public PhysicalPlan getPlan(SqlNode sqlNode) throws ValidationException, RelConv
final boolean checkTableNonExistence = sqlCreateTable.checkTableNonExistence();
final String schemaPath = drillSchema.getFullSchemaName();
+ // Ranger DDL authorization: CREATE privilege on the new table. Temporary
+ // tables are session-scoped objects (UUID name, invisible to other users)
+ // and bypass authorization. Checked before the existence check so an
+ // unauthorized user cannot probe table existence via differing errors.
+ if (!sqlCreateTable.isTemporary() && AccessAuthorizerManager.isEnabled(config.getContext().getConfig())) {
+ DdlAccessChecker.checkDdlAccess(context, drillSchema, originalTableName, AccessType.CREATE);
+ }
+
// Check table creation possibility
if(!checkTableCreationPossibility(drillSchema, originalTableName, drillConfig, context.getSession(), schemaPath, checkTableNonExistence)) {
return DirectPlan.createDirectPlan(context, false,
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/DropTableHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/DropTableHandler.java
index c9f5e7f5bb3..69148961820 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/DropTableHandler.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/DropTableHandler.java
@@ -31,6 +31,9 @@
import org.apache.drill.exec.planner.sql.SchemaUtilities;
import org.apache.drill.exec.planner.sql.parser.SqlDropTable;
import org.apache.drill.exec.rpc.user.UserSession;
+import org.apache.drill.exec.security.AccessAuthorizerManager;
+import org.apache.drill.exec.security.DdlAccessChecker;
+import org.apache.drill.exec.security.spi.AccessType;
import org.apache.drill.exec.store.AbstractSchema;
// SqlHandler for dropping a table.
@@ -67,6 +70,13 @@ public PhysicalPlan getPlan(SqlNode sqlNode) {
session.removeTemporaryTable(temporarySchema, originalTableName, drillConfig);
} else {
AbstractSchema drillSchema = SchemaUtilities.resolveToMutableDrillSchema(defaultSchema, tableSchema);
+
+ // Ranger DDL authorization: DROP privilege on the target table.
+ // Checked before the existence check so an unauthorized user cannot
+ // probe table existence via differing errors.
+ if (AccessAuthorizerManager.isEnabled(config.getContext().getConfig())) {
+ DdlAccessChecker.checkDdlAccess(context, drillSchema, originalTableName, AccessType.DROP);
+ }
Table tableToDrop = SqlHandlerUtil.getTableFromSchema(drillSchema, originalTableName);
// TableType.OTHER started getting reported for H2 DB when it was upgraded to v2.
if (tableToDrop == null || (tableToDrop.getJdbcTableType() != Schema.TableType.TABLE &&
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/ViewHandler.java b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/ViewHandler.java
index 089de0ce83d..94a8f31519c 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/ViewHandler.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/planner/sql/handlers/ViewHandler.java
@@ -35,6 +35,9 @@
import org.apache.drill.exec.planner.sql.SchemaUtilities;
import org.apache.drill.exec.planner.sql.parser.SqlCreateView;
import org.apache.drill.exec.planner.sql.parser.SqlDropView;
+import org.apache.drill.exec.security.AccessAuthorizerManager;
+import org.apache.drill.exec.security.DdlAccessChecker;
+import org.apache.drill.exec.security.spi.AccessType;
import org.apache.drill.exec.store.AbstractSchema;
import org.apache.drill.exec.work.foreman.ForemanSetupException;
import org.apache.calcite.rel.RelNode;
@@ -81,6 +84,14 @@ public PhysicalPlan getPlan(SqlNode sqlNode) throws ValidationException, RelConv
SchemaUtilities.getSchemaPathAsList(defaultSchema));
final String schemaPath = drillSchema.getFullSchemaName();
+ // Ranger DDL authorization: CREATE privilege on the new view.
+ // OR REPLACE is treated as a single CREATE operation (no extra DROP
+ // requirement). Checked before the existence check so an unauthorized
+ // user cannot probe view existence via differing errors.
+ if (AccessAuthorizerManager.isEnabled(config.getContext().getConfig())) {
+ DdlAccessChecker.checkDdlAccess(context, drillSchema, newViewName, AccessType.CREATE);
+ }
+
// check view creation possibility
if(!checkViewCreationPossibility(drillSchema, createView, context)) {
return DirectPlan
@@ -163,6 +174,13 @@ public PhysicalPlan getPlan(SqlNode sqlNode) throws IOException, ForemanSetupExc
final String schemaPath = drillSchema.getFullSchemaName();
+ // Ranger DDL authorization: DROP privilege on the target view.
+ // Checked before the existence check so an unauthorized user cannot
+ // probe view existence via differing errors.
+ if (AccessAuthorizerManager.isEnabled(config.getContext().getConfig())) {
+ DdlAccessChecker.checkDdlAccess(context, drillSchema, viewName, AccessType.DROP);
+ }
+
final Table viewToDrop = SqlHandlerUtil.getTableFromSchema(drillSchema, viewName);
if (dropView.checkViewExistence()) {
if (viewToDrop == null || viewToDrop.getJdbcTableType() != Schema.TableType.VIEW){
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/security/AccessAuthorizerManager.java b/exec/java-exec/src/main/java/org/apache/drill/exec/security/AccessAuthorizerManager.java
new file mode 100644
index 00000000000..fc61c6b6d6b
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/security/AccessAuthorizerManager.java
@@ -0,0 +1,189 @@
+/*
+ * 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.drill.exec.security;
+
+import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessAuthorizerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+/**
+ * Engine-side manager for the {@link AccessAuthorizer} SPI.
+ *
+ * - reads {@code drill.exec.security.authorizer.enabled} — when {@code false}
+ * (the default) an {@link AllowAllAccessAuthorizer} is used (fail-open);
+ * - flattens the {@code drill.exec.security.authorizer} configuration
+ * subtree into a plain {@code Map} (engine configuration
+ * types never leak into the SPI);
+ * - discovers {@link AccessAuthorizerFactory} implementations via
+ * {@link ServiceLoader} and selects the one whose {@code getName()}
+ * matches {@code drill.exec.security.authorizer.name} (default
+ * {@code "ranger"});
+ * - fail-closed: enabled with no matching factory throws at startup
+ * rather than silently allowing everything.
+ *
+ *
+ * The created instance is cached in a lazy double-checked-locking singleton;
+ * mount points ({@code DrillCalciteCatalogReader}, {@code ColumnAccessChecker})
+ * retrieve it via {@link #getAuthorizer(DrillConfig)}. The Drillbit triggers
+ * initialization eagerly at startup for fail-fast behavior.
+ */
+public final class AccessAuthorizerManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(AccessAuthorizerManager.class);
+
+ static final String CONFIG_PREFIX = "drill.exec.security.authorizer";
+ static final String DEFAULT_FACTORY_NAME = "ranger";
+
+ private static volatile AccessAuthorizer instance;
+
+ private AccessAuthorizerManager() {
+ }
+
+ /**
+ * Returns the singleton {@link AccessAuthorizer} instance, initializing it
+ * from the given configuration on first call.
+ *
+ * @param config the Drill configuration
+ * @return the authorizer (never {@code null})
+ */
+ public static AccessAuthorizer getAuthorizer(DrillConfig config) {
+ if (instance != null) {
+ return instance;
+ }
+ synchronized (AccessAuthorizerManager.class) {
+ if (instance == null) {
+ instance = load(config);
+ }
+ }
+ return instance;
+ }
+
+ /**
+ * Resets the cached singleton. Package-private; used by tests.
+ */
+ static void reset() {
+ instance = null;
+ }
+
+ /**
+ * Closes the cached authorizer if any, then clears the singleton so a
+ * future {@link #getAuthorizer(DrillConfig)} initializes a fresh instance.
+ *
+ * Called by {@code Drillbit.close()} during shutdown, after all
+ * in-flight queries have drained: the authorizer may hold plugin resources
+ * (e.g. Ranger policy-refresh threads, policy-engine caches) that must be
+ * released. Idempotent and safe when authorization is disabled or never
+ * initialized — {@link AllowAllAccessAuthorizer} performs no work, and the
+ * default SPI {@code close()} is a no-op. Close failures are logged and
+ * ignored so a failing authorizer never aborts Drill shutdown.
+ */
+ public static void close() {
+ synchronized (AccessAuthorizerManager.class) {
+ AccessAuthorizer authorizer = instance;
+ // Drop the singleton reference first: even if close() throws, the next
+ // getAuthorizer() must be able to build a fresh instance.
+ instance = null;
+ if (authorizer != null) {
+ try {
+ authorizer.close();
+ } catch (Exception e) {
+ logger.warn("Failure while closing access authorizer", e);
+ }
+ }
+ }
+ }
+
+ private static AccessAuthorizer load(DrillConfig config) {
+ if (!isEnabled(config)) {
+ logger.info("Access authorizer disabled (drill.exec.security.authorizer.enabled=false); "
+ + "using AllowAllAccessAuthorizer (fail-open)");
+ return new AllowAllAccessAuthorizer();
+ }
+
+ // Read the selection key directly from the config: flattenConfig strips
+ // the engine-managed keys ("enabled"/"name") from the plugin-visible map.
+ String name = config.hasPath(CONFIG_PREFIX + ".name")
+ ? config.getString(CONFIG_PREFIX + ".name") : null;
+ if (name == null || name.isEmpty()) {
+ name = DEFAULT_FACTORY_NAME;
+ }
+ Map props = flattenConfig(config);
+
+ // Discover factories via the Java SPI. The Ranger factory is shipped on
+ // the Drillbit classpath (shim jar); Drill core has no compile-time
+ // reference to any implementation.
+ for (AccessAuthorizerFactory factory : ServiceLoader.load(AccessAuthorizerFactory.class)) {
+ if (factory.getName().equals(name)) {
+ AccessAuthorizer authorizer = factory.createAuthorizer(props);
+ logger.info("Initialized access authorizer '{}' via {}", name,
+ factory.getClass().getName());
+ return authorizer;
+ }
+ logger.debug("Skipping AccessAuthorizerFactory '{}' (does not match configured name '{}')",
+ factory.getName(), name);
+ }
+ throw new RuntimeException("Access authorizer is enabled but no AccessAuthorizerFactory "
+ + "named '" + name + "' was found on the classpath. Ensure the shim jar registering "
+ + "META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory "
+ + "is present and initialized correctly.");
+ }
+
+ /**
+ * Returns whether the access authorizer is enabled in the configuration
+ * ({@code drill.exec.security.authorizer.enabled}, default {@code false}).
+ *
+ * Mount points that pay measurable setup cost before the first SPI call
+ * (e.g. the column-level check walks the whole RelNode tree) consult this
+ * up front and skip the work entirely when authorization is disabled,
+ * instead of relying on the {@link AllowAllAccessAuthorizer} sentinel
+ * short-circuiting inside each check.
+ *
+ * @param config the Drill configuration
+ * @return {@code true} only when the enabled flag is explicitly set to true
+ */
+ public static boolean isEnabled(DrillConfig config) {
+ return config.hasPath(CONFIG_PREFIX + ".enabled")
+ && config.getBoolean(CONFIG_PREFIX + ".enabled");
+ }
+
+ /**
+ * Flattens the {@code drill.exec.security.authorizer} configuration subtree
+ * into a plain map of scalar leaf values. The {@code enabled} and {@code name}
+ * keys are engine-managed selection knobs and are removed from the result.
+ */
+ private static Map flattenConfig(DrillConfig config) {
+ Map props = new HashMap<>();
+ if (config.hasPath(CONFIG_PREFIX)) {
+ config.getConfig(CONFIG_PREFIX).entrySet().forEach(e -> {
+ Object value = e.getValue().unwrapped();
+ if (value != null && !(value instanceof Map) && !(value instanceof List)) {
+ props.put(e.getKey(), String.valueOf(value));
+ }
+ });
+ }
+ props.remove("enabled");
+ props.remove("name");
+ return props;
+ }
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/security/AllowAllAccessAuthorizer.java b/exec/java-exec/src/main/java/org/apache/drill/exec/security/AllowAllAccessAuthorizer.java
new file mode 100644
index 00000000000..7344c11e0ec
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/security/AllowAllAccessAuthorizer.java
@@ -0,0 +1,44 @@
+/*
+ * 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.drill.exec.security;
+
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+
+import java.util.Set;
+
+/**
+ * Engine-default implementation that allows all access (mirrors Presto's
+ * {@code AllowAllAccessControl}). Used when the access authorizer is
+ * disabled: the engine calls it through the same {@link AccessAuthorizer}
+ * interface, so mount points need no special-casing for the disabled state.
+ */
+public class AllowAllAccessAuthorizer implements AccessAuthorizer {
+
+ @Override
+ public boolean checkTableAccess(UserIdentity user, String dataSource, String schema,
+ String table, AccessType accessType) {
+ return true; // fail-open
+ }
+
+ @Override
+ public boolean checkColumnAccess(UserIdentity user, String dataSource, String schema,
+ String table, Set columns, AccessType accessType) {
+ return true; // fail-open
+ }
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/security/DdlAccessChecker.java b/exec/java-exec/src/main/java/org/apache/drill/exec/security/DdlAccessChecker.java
new file mode 100644
index 00000000000..431f180c2a7
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/security/DdlAccessChecker.java
@@ -0,0 +1,86 @@
+/*
+ * 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.drill.exec.security;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.common.exceptions.UserException;
+import org.apache.drill.exec.ops.QueryContext;
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.apache.drill.exec.store.AbstractSchema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * DDL authorization checks for the handlers that create or drop tables and
+ * views ({@code CREATE TABLE} / CTAS, {@code DROP TABLE}, {@code CREATE VIEW},
+ * {@code DROP VIEW}).
+ *
+ * Statements that go through the Calcite catalog reader (queries, and the
+ * query part of CTAS / CREATE VIEW) are checked in
+ * {@code DrillCalciteCatalogReader}. DDL statements address their target
+ * object directly through the schema, so the check is issued here at the
+ * handler layer.
+ *
+ * Resource mapping reuses {@link TableAccessResource#resolve(List)} so
+ * table-level, column-level and DDL checks address exactly the same resource
+ * for the same table. No-op when authorization is disabled (the manager
+ * returns the allow-all authorizer).
+ */
+public final class DdlAccessChecker {
+
+ private static final Logger logger = LoggerFactory.getLogger(DdlAccessChecker.class);
+
+ private DdlAccessChecker() {
+ }
+
+ /**
+ * Checks CREATE/DROP permission on the DDL target object (table or view)
+ * via the configured {@link AccessAuthorizer} (Ranger by default). Throws
+ * {@link UserException} permissionError when access is denied.
+ *
+ * Authorization happens before the existence check ("table not found")
+ * so that an unauthorized user cannot probe object existence through
+ * differing error messages.
+ *
+ * @param context query context (session + config)
+ * @param schema the resolved schema holding the target object
+ * @param objectName the target table/view name as written by the user
+ * @param accessType the access type ({@link AccessType#CREATE} or
+ * {@link AccessType#DROP})
+ */
+ public static void checkDdlAccess(QueryContext context, AbstractSchema schema,
+ String objectName, AccessType accessType) {
+ List qualifiedName = new ArrayList<>(schema.getSchemaPath());
+ qualifiedName.add(objectName);
+ TableAccessResource resource = TableAccessResource.resolve(qualifiedName);
+ String userName = context.getSession().getCredentials().getUserName();
+ DrillConfig drillConfig = context.getConfig();
+ AccessAuthorizer authorizer = AccessAuthorizerManager.getAuthorizer(drillConfig);
+ if (!authorizer.checkTableAccess(UserIdentity.of(userName), resource.getDataSource(),
+ resource.getSchemaPath(), resource.getTable(), accessType)) {
+ throw UserException.permissionError()
+ .message("Access denied: user '%s' lacks %s privilege on %s",
+ userName, accessType, resource)
+ .build(logger);
+ }
+ }
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/security/TableAccessResource.java b/exec/java-exec/src/main/java/org/apache/drill/exec/security/TableAccessResource.java
new file mode 100644
index 00000000000..694a5178eb6
--- /dev/null
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/security/TableAccessResource.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.drill.exec.security;
+
+import org.apache.drill.exec.planner.sql.SchemaUtilities;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The (datasource, schema, table) triple that identifies a table in an access
+ * check, following the Ranger four-level resource model
+ * {@code datasource / schema / table / column}.
+ *
+ * Use {@link #resolve(List)} as the single mapping from a Calcite
+ * table's qualified name to this triple. The mapping MUST be defined exactly
+ * once: table-level checks (DrillCalciteCatalogReader) and column-level checks
+ * (ColumnAccessChecker) must address the same resource for the same table,
+ * otherwise one check may match a Ranger policy while the other does not —
+ * and since Ranger denies by default, that shows up as an inconsistent
+ * allow/deny depending on which check fires first.
+ */
+public final class TableAccessResource {
+
+ private final String dataSource;
+ private final String schemaPath;
+ private final String table;
+
+ private TableAccessResource(String dataSource, String schemaPath, String table) {
+ this.dataSource = dataSource;
+ this.schemaPath = schemaPath;
+ this.table = table;
+ }
+
+ /**
+ * Resolves the access-check resource from a table's qualified name.
+ *
+ * The first segment of a qualified name is the datasource (storage
+ * plugin name), the last segment is the table, and any segments in between
+ * form the schema path. The schema MUST NOT include the datasource prefix,
+ * otherwise policy matching fails (policy has {@code schema=shf} but the
+ * request would send {@code schema=mysql.shf}).
+ *
+ * Some backends have no schema concept (e.g. a flat file store). To keep
+ * the four-level model uniform, a default schema is synthesized per
+ * datasource via {@link #getDefaultSchemaByDataSource(String)}.
+ *
+ * @param qualifiedName the Calcite-resolved qualified name (never null or
+ * empty), e.g. {@code [mysql, shf, orders]},
+ * {@code [cp, employee.json]} or {@code [orders]}
+ * @return the resolved (datasource, schema, table) triple
+ */
+ public static TableAccessResource resolve(List qualifiedName) {
+ Objects.requireNonNull(qualifiedName, "qualifiedName must not be null");
+ if (qualifiedName.isEmpty()) {
+ throw new IllegalArgumentException("qualifiedName must not be empty");
+ }
+
+ String table = qualifiedName.get(qualifiedName.size() - 1);
+ if (qualifiedName.size() > 2) {
+ // datasource.schema.table OR datasource.subschema.table
+ String dataSource = qualifiedName.get(0);
+ String schemaPath = SchemaUtilities.getSchemaPath(
+ qualifiedName.subList(1, qualifiedName.size() - 1));
+ return new TableAccessResource(dataSource, schemaPath, table);
+ }
+ if (qualifiedName.size() == 2) {
+ // datasource.table — backend has no schema; synthesize a default so the
+ // four-level resource stays complete (policy matching requires a
+ // non-null schema key when schema is a mandatory resource).
+ String dataSource = qualifiedName.get(0);
+ return new TableAccessResource(dataSource, getDefaultSchemaByDataSource(dataSource), table);
+ }
+ // Single-element qualified name: the table is registered at the root
+ // schema. Use the table name itself as the datasource namespace.
+ return new TableAccessResource(table, getDefaultSchemaByDataSource(table), table);
+ }
+
+ /**
+ * Returns the default schema name to use when a table's qualified name does
+ * not contain an explicit schema segment (i.e. two-segment
+ * {@code datasource.table} or a single-segment fallback). This keeps the
+ * four-level resource model complete even for backends that have no native
+ * schema concept.
+ *
+ * Add explicit cases below as new storage plugins are integrated. The
+ * {@code default} branch returns the datasource name itself so each plugin
+ * gets a distinct default schema namespace without further configuration.
+ *
+ * @param dataSource the storage plugin / datasource name
+ * @return a non-null default schema name
+ */
+ private static String getDefaultSchemaByDataSource(String dataSource) {
+ return switch (dataSource.toLowerCase()) {
+ case "dfs", "cp" -> "default";
+ default -> dataSource;
+ };
+ }
+
+ public String getDataSource() {
+ return dataSource;
+ }
+
+ public String getSchemaPath() {
+ return schemaPath;
+ }
+
+ public String getTable() {
+ return table;
+ }
+
+ @Override
+ public String toString() {
+ return dataSource + "." + schemaPath + "." + table;
+ }
+}
diff --git a/exec/java-exec/src/main/java/org/apache/drill/exec/server/Drillbit.java b/exec/java-exec/src/main/java/org/apache/drill/exec/server/Drillbit.java
index cb78340f7e4..d1c168d3595 100644
--- a/exec/java-exec/src/main/java/org/apache/drill/exec/server/Drillbit.java
+++ b/exec/java-exec/src/main/java/org/apache/drill/exec/server/Drillbit.java
@@ -34,6 +34,7 @@
import org.apache.drill.exec.exception.DrillbitStartupException;
import org.apache.drill.exec.proto.CoordinationProtos.DrillbitEndpoint;
import org.apache.drill.exec.proto.CoordinationProtos.DrillbitEndpoint.State;
+import org.apache.drill.exec.security.AccessAuthorizerManager;
import org.apache.drill.exec.server.DrillbitStateManager.DrillbitState;
import org.apache.drill.exec.server.options.OptionDefinition;
import org.apache.drill.exec.server.options.OptionValue;
@@ -229,6 +230,9 @@ public void run() throws Exception {
final DrillbitContext drillbitContext = manager.getContext();
storageRegistry = drillbitContext.getStorage();
storageRegistry.init();
+ // Initialize the access authorizer if enabled (fail-fast at startup; the
+ // cached singleton is later reused by planner mount points)
+ AccessAuthorizerManager.getAuthorizer(context.getConfig());
drillbitContext.getOptionManager().init();
javaPropertiesToSystemOptions();
manager.getContext().getRemoteFunctionRegistry().init(context.getConfig(), storeProvider, coord);
@@ -333,6 +337,12 @@ public synchronized void close() {
logger.warn("Failure on close()", e);
}
+ // Release access-authorizer plugin resources (e.g. Ranger policy-refresh
+ // threads, policy-engine caches) now that all queries have drained and the
+ // engine services above are closed. Idempotent; safe when authorization is
+ // disabled or was never initialized.
+ AccessAuthorizerManager.close();
+
logger.info("Shutdown completed ({} ms).", w.elapsed(TimeUnit.MILLISECONDS) );
stateManager.setState(DrillbitState.SHUTDOWN);
// Interrupt GracefulShutdownThread since Drillbit close is not called from it.
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/planner/sql/conversion/ColumnAccessCheckerCorrelatedTest.java b/exec/java-exec/src/test/java/org/apache/drill/exec/planner/sql/conversion/ColumnAccessCheckerCorrelatedTest.java
new file mode 100644
index 00000000000..b00c77cb7b6
--- /dev/null
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/planner/sql/conversion/ColumnAccessCheckerCorrelatedTest.java
@@ -0,0 +1,479 @@
+/*
+ * 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.drill.exec.planner.sql.conversion;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.calcite.linq4j.tree.Expression;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptSchema;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollationTraitDef;
+import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.RelDistribution;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelReferentialConstraint;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.core.CorrelationId;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.logical.LogicalSort;
+import org.apache.calcite.rel.logical.LogicalTableScan;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+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.rex.RexBuilder;
+import org.apache.calcite.rex.RexCorrelVariable;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexSubQuery;
+import org.apache.calcite.schema.ColumnStrategy;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.exec.proto.UserBitShared;
+import org.apache.drill.exec.rpc.user.UserSession;
+import org.apache.drill.test.BaseTest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Pure JUnit tests for {@link ColumnAccessChecker} focused on correlated
+ * subquery outer-column references.
+ *
+ * These tests DO NOT start a Drillbit, load any storage plugin, or require
+ * Ranger to be enabled. They build a Calcite RelNode tree directly and feed
+ * it to {@code ColumnAccessChecker.check()}. When Ranger is disabled (the
+ * default), {@code AccessAuthorizerManager} returns a no-op authorizer, so no
+ * real enforcement decision is made. Instead, we inspect the bookkeeping map
+ * {@code ColumnAccessChecker.tableToReferencedCols} via reflection after the
+ * visit completes. That map records origin table-column indices for every
+ * column reference the checker discovered, including references traced
+ * through correlated outer-column resolution.
+ *
+ *
Covered scenarios:
+ *
+ * - Simple correlated EXISTS referencing a column the outer SELECT does
+ * NOT project — the exact gap flagged by the reviewer at
+ * ColumnAccessChecker:323 (Doc case 13 masked it with {@code SELECT *}).
+ * - Two nested EXISTS subqueries where the innermost does a skip-level
+ * correlation back to the outermost table — verifies
+ * {@code putIfAbsent(varName, enclosingScope)} semantics so an
+ * intermediate enclosing scope does not clobber the outer binding.
+ *
+ */
+public class ColumnAccessCheckerCorrelatedTest extends BaseTest {
+
+ // Column indices for both test tables. The two tables share an identical
+ // field layout (same field names at the same ordinal positions) so that any
+ // row-type-based heuristic would be unable to distinguish them; an
+ // exact-name binding has no ambiguity here.
+ //
+ // orders: [id(0 INT), amount(1 DECIMAL), user_id(2 INT), active(3 BOOLEAN)]
+ // customers:[id(0 INT), name(1 VARCHAR), user_id(2 INT)]
+ private static final int COL_ID = 0;
+ private static final int COL_USER_ID = 2;
+
+ private RelOptCluster cluster;
+ private RexBuilder rexBuilder;
+ private SqlTypeFactoryImpl typeFactory;
+ private RelOptTable ordersTable;
+ private RelOptTable customersTable;
+
+ @Before
+ public void setUp() {
+ typeFactory = new SqlTypeFactoryImpl(
+ org.apache.calcite.rel.type.RelDataTypeSystem.DEFAULT);
+ rexBuilder = new RexBuilder(typeFactory);
+
+ VolcanoPlanner planner = new VolcanoPlanner();
+ planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+ planner.addRelTraitDef(RelCollationTraitDef.INSTANCE);
+
+ cluster = RelOptCluster.create(planner, rexBuilder);
+ cluster.setMetadataQuerySupplier(RelMetadataQuery::instance);
+
+ ordersTable = new StubRelOptTable(
+ ImmutableList.of("cp", "default", "orders"),
+ buildOrdersRowType(),
+ typeFactory);
+ customersTable = new StubRelOptTable(
+ ImmutableList.of("cp", "default", "customers"),
+ buildCustomersRowType(),
+ typeFactory);
+ }
+
+ private RelDataType buildOrdersRowType() {
+ return typeFactory.builder()
+ .add("id", typeFactory.createSqlType(SqlTypeName.INTEGER))
+ .add("amount", typeFactory.createSqlType(SqlTypeName.DECIMAL))
+ .add("user_id", typeFactory.createSqlType(SqlTypeName.INTEGER))
+ .add("active", typeFactory.createSqlType(SqlTypeName.BOOLEAN))
+ .build();
+ }
+
+ private RelDataType buildCustomersRowType() {
+ return typeFactory.builder()
+ .add("id", typeFactory.createSqlType(SqlTypeName.INTEGER))
+ .add("name", typeFactory.createSqlType(SqlTypeName.VARCHAR, 64))
+ .add("user_id", typeFactory.createSqlType(SqlTypeName.INTEGER))
+ .build();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ Field f = Class.forName(
+ "org.apache.drill.exec.security.AccessAuthorizerManager")
+ .getDeclaredField("instance");
+ f.setAccessible(true);
+ f.set(null, null);
+ }
+
+ // ------------------------------------------------------------------
+ // Helpers
+ // ------------------------------------------------------------------
+
+ @SuppressWarnings("unchecked")
+ private Map> runCheck(RelNode root) throws Exception {
+ UserBitShared.UserCredentials creds = UserBitShared.UserCredentials.newBuilder()
+ .setUserName("alice").build();
+ UserSession session = Mockito.mock(UserSession.class);
+ Mockito.when(session.getCredentials()).thenReturn(creds);
+
+ DrillConfig drillConfig = DrillConfig.create();
+ RelMetadataQuery mq = cluster.getMetadataQuery();
+ ColumnAccessChecker checker = new ColumnAccessChecker(session, drillConfig, mq);
+ // Bookkeeping map is populated before enforceColumnAccess is called, so
+ // the default no-op authorizer is sufficient for our purposes.
+ checker.check(root);
+
+ Field f = ColumnAccessChecker.class.getDeclaredField("tableToReferencedCols");
+ f.setAccessible(true);
+ return (Map>) f.get(checker);
+ }
+
+ private static void assertRefColumns(RelOptTable table,
+ Map> state,
+ Integer... expectedIndices) {
+ Set actual = state.get(table);
+ assertNotNull("Expected references for table " + table.getQualifiedName()
+ + " but none were recorded", actual);
+ Set expected = new HashSet<>(Arrays.asList(expectedIndices));
+ assertEquals("Referenced column indices for " + table.getQualifiedName()
+ + " differ. expected=" + expected + " actual=" + actual,
+ expected, actual);
+ }
+
+ private static void assertRefContains(RelOptTable table,
+ Map> state, int index) {
+ Set actual = state.get(table);
+ assertNotNull("Expected references for " + table.getQualifiedName()
+ + " but none were recorded", actual);
+ assertTrue("Expected column index " + index
+ + " to be recorded for " + table.getQualifiedName()
+ + ", actual=" + actual,
+ actual.contains(index));
+ }
+
+ private static RelDataType intLiteralRowType(SqlTypeFactoryImpl tf) {
+ return tf.createStructType(
+ Collections.singletonList(tf.createSqlType(SqlTypeName.INTEGER)),
+ Collections.singletonList("$f0"));
+ }
+
+ // ------------------------------------------------------------------
+ // Tests
+ // ------------------------------------------------------------------
+
+ /**
+ * SQL shape:
+ *
+ * SELECT o.id
+ * FROM orders o
+ * WHERE EXISTS (
+ * SELECT 1 FROM customers c
+ * WHERE c.user_id = o.user_id
+ * );
+ *
+ *
+ * The outer SELECT projects only {@code o.id} (column 0). The inner
+ * predicate references {@code o.user_id} (column 2) via a correlated
+ * variable {@code $cor0.user_id}. Before the fix,
+ * {@code RexFieldAccess(RexCorrelVariable)} was silently skipped so
+ * orders.user_id would be missing from the traced column set — a classic
+ * column-level bypass.
+ */
+ @Test
+ public void correlatedExistsRefsUnprojectedOuterColumn() throws Exception {
+ RelNode ordersScan = LogicalTableScan.create(cluster, ordersTable, Collections.emptyList());
+ CorrelationId corr0Id = cluster.createCorrel();
+ RelNode customersScan = LogicalTableScan.create(cluster, customersTable, Collections.emptyList());
+
+ RexCorrelVariable cor0 = (RexCorrelVariable)
+ rexBuilder.makeCorrel(ordersTable.getRowType(), corr0Id);
+ RexNode outerUserId = rexBuilder.makeFieldAccess(cor0, COL_USER_ID);
+ RexNode innerUserId = rexBuilder.makeInputRef(customersScan, COL_USER_ID);
+ RexNode innerCond = rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
+ innerUserId, outerUserId);
+ RelNode filteredCustomers = LogicalFilter.create(customersScan, innerCond);
+ RelNode innerProject = LogicalProject.create(filteredCustomers,
+ Collections.emptyList(),
+ Collections.singletonList(rexBuilder.makeExactLiteral(BigDecimal.ONE)),
+ intLiteralRowType(typeFactory));
+
+ RexSubQuery existsSq = RexSubQuery.exists(innerProject);
+ RelNode filteredOrders = LogicalFilter.create(ordersScan, existsSq);
+
+ RelNode outerProject = LogicalProject.create(filteredOrders,
+ Collections.emptyList(),
+ Collections.singletonList(rexBuilder.makeInputRef(filteredOrders, COL_ID)),
+ typeFactory.createStructType(
+ Collections.singletonList(typeFactory.createSqlType(SqlTypeName.INTEGER)),
+ Collections.singletonList("id")));
+
+ Map> refs = runCheck(outerProject);
+
+ // orders: id (0) from outer SELECT projection + user_id (2) from the
+ // correlated predicate captured by the new visitFieldAccess handler.
+ assertRefColumns(ordersTable, refs, COL_ID, COL_USER_ID);
+
+ // customers: the inner filter used user_id (column 2).
+ assertRefContains(customersTable, refs, COL_USER_ID);
+ }
+
+ /**
+ * SQL shape:
+ *
+ * SELECT o.id
+ * FROM orders o
+ * WHERE EXISTS (
+ * SELECT 1 FROM customers c
+ * WHERE c.id = o.id
+ * AND EXISTS (
+ * SELECT 1 FROM customers c2
+ * WHERE c2.user_id = o.user_id -- skip-level correlation
+ * )
+ * );
+ *
+ *
+ * The innermost subquery references the outermost scope's user_id column.
+ * Middle and outer scopes share an identical row schema, so a row-type-based
+ * scope resolver would be ambiguous. The exact-name binding implemented in
+ * ColumnAccessChecker remembers the $corN → scope mapping captured at
+ * first encounter and never overwrites it for skip-level reuses.
+ */
+ @Test
+ public void nestedExistsSkipLevelCorrelation_preservesExactBinding() throws Exception {
+ RelNode ordersScan = LogicalTableScan.create(cluster, ordersTable, Collections.emptyList());
+
+ CorrelationId corr0Id = cluster.createCorrel();
+ CorrelationId corr1Id = cluster.createCorrel();
+ RexCorrelVariable cor0 = (RexCorrelVariable)
+ rexBuilder.makeCorrel(ordersTable.getRowType(), corr0Id);
+
+ // Innermost: c2.user_id = $cor0.user_id (SKIP LEVEL — not $cor1)
+ RelNode c2Scan = LogicalTableScan.create(cluster, customersTable, Collections.emptyList());
+ RexNode c2UserId = rexBuilder.makeInputRef(c2Scan, COL_USER_ID);
+ RexNode skipOuterUserId = rexBuilder.makeFieldAccess(cor0, COL_USER_ID);
+ RexNode innerCond = rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
+ c2UserId, skipOuterUserId);
+ RelNode filteredC2 = LogicalFilter.create(c2Scan, innerCond);
+ RelNode innerProject = LogicalProject.create(filteredC2,
+ Collections.emptyList(),
+ Collections.singletonList(rexBuilder.makeExactLiteral(BigDecimal.ONE)),
+ intLiteralRowType(typeFactory));
+ RexSubQuery innerExists = RexSubQuery.exists(innerProject);
+
+ // Middle customers: c.id = $cor0.id AND EXISTS(...)
+ RelNode cScan = LogicalTableScan.create(cluster, customersTable, Collections.emptyList());
+ RexNode cId = rexBuilder.makeInputRef(cScan, COL_ID);
+ RexNode outerIdRef = rexBuilder.makeFieldAccess(cor0, COL_ID);
+ RexNode firstCond = rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
+ cId, outerIdRef);
+ RexNode middleCond = rexBuilder.makeCall(SqlStdOperatorTable.AND,
+ firstCond, innerExists);
+ RelNode filteredC = LogicalFilter.create(cScan, middleCond);
+ RelNode middleProject = LogicalProject.create(filteredC,
+ Collections.emptyList(),
+ Collections.singletonList(rexBuilder.makeExactLiteral(BigDecimal.ONE)),
+ intLiteralRowType(typeFactory));
+ RexSubQuery outerExists = RexSubQuery.exists(middleProject);
+
+ RelNode filteredOrders = LogicalFilter.create(ordersScan, outerExists);
+ RelNode outerProject = LogicalProject.create(filteredOrders,
+ Collections.emptyList(),
+ Collections.singletonList(rexBuilder.makeInputRef(filteredOrders, COL_ID)),
+ typeFactory.createStructType(
+ Collections.singletonList(typeFactory.createSqlType(SqlTypeName.INTEGER)),
+ Collections.singletonList("id")));
+
+ Map> refs = runCheck(outerProject);
+
+ // orders: id (0) from outer SELECT + outer $cor0.id + skip-level
+ // $cor0.user_id (column 2).
+ assertRefColumns(ordersTable, refs, COL_ID, COL_USER_ID);
+
+ // customers: id (0) from middle predicate + user_id (2) from innermost.
+ assertRefContains(customersTable, refs, COL_ID);
+ assertRefContains(customersTable, refs, COL_USER_ID);
+ }
+
+ /**
+ * SQL shape:
+ *
+ * SELECT SUM(amount) FILTER (WHERE active) FROM orders
+ *
+ *
+ * The aggregate has no GROUP BY and one AggregateCall SUM(amount) with a
+ * FILTER clause referencing the boolean column active (filterArg=3).
+ * Before the fix, only {@code getGroupSet()} was traced (empty here), so
+ * neither amount (argList) nor active (filterArg) would be recorded. The
+ * fix traces {@code getAggCallList()} args, filterArg, and collation
+ * explicitly.
+ */
+ @Test
+ public void aggregateCallArgsAndFilterAreTraced() throws Exception {
+ RelNode ordersScan = LogicalTableScan.create(cluster, ordersTable, Collections.emptyList());
+
+ // SUM(amount): argList=[1], filterArg=3 (active BOOLEAN NOT NULL column)
+ AggregateCall sumCall = AggregateCall.create(
+ SqlStdOperatorTable.SUM,
+ false, // distinct
+ false, // approximate
+ false, // ignoreNulls
+ ImmutableList.of(1), // argList: amount
+ 3, // filterArg: active (BOOLEAN NOT NULL)
+ null, // distinctKeys
+ RelCollations.EMPTY, // collation
+ typeFactory.createTypeWithNullability(
+ typeFactory.createSqlType(SqlTypeName.DECIMAL), true),
+ "SUM(amount)");
+
+ RelNode aggregate = LogicalAggregate.create(ordersScan,
+ ImmutableBitSet.of(), // groupSet: empty (no GROUP BY)
+ ImmutableList.of(ImmutableBitSet.of()), // groupSets
+ Collections.singletonList(sumCall));
+
+ Map> refs = runCheck(aggregate);
+
+ // amount (1) from argList + active (3) from filterArg — both must be present
+ assertRefContains(ordersTable, refs, 1);
+ assertRefContains(ordersTable, refs, 3);
+ }
+
+ /**
+ * SQL shape:
+ *
+ * SELECT * FROM orders ORDER BY id LIMIT
+ *
+ *
+ * The LIMIT (fetch) expression is a RexNode that may carry column references
+ * or subqueries. Before the fix, {@code visit(LogicalSort)} only traced the
+ * ORDER BY collation and never called {@code analyzeRex} on fetch/offset,
+ * so any column reachable only from LIMIT/OFFSET was silently skipped.
+ */
+ @Test
+ public void sortFetchExpressionIsAnalyzed() throws Exception {
+ RelNode ordersScan = LogicalTableScan.create(cluster, ordersTable, Collections.emptyList());
+
+ // ORDER BY id (column 0); LIMIT references amount (column 1).
+ // Without the fix, only column 0 (from collation) would be traced;
+ // column 1 (from fetch) would be missed.
+ RexNode fetch = rexBuilder.makeInputRef(ordersScan, 1);
+ RelNode sort = LogicalSort.create(ordersScan,
+ RelCollations.of(0), // ORDER BY id
+ fetch,
+ null); // no offset
+
+ Map> refs = runCheck(sort);
+
+ // id (0) from collation + amount (1) from fetch — both must be present
+ assertRefContains(ordersTable, refs, 0);
+ assertRefContains(ordersTable, refs, 1);
+ }
+
+ // ------------------------------------------------------------------
+ // Stub RelOptTable: avoids the finicky RelOptTableImpl factory overloads
+ // while satisfying LogicalTableScan + RelMetadataQuery column-origin
+ // tracing. Only the methods reached by ColumnAccessChecker +
+ // LogicalTableScan + MQ.getColumnOrigins are implemented meaningfully.
+ // ------------------------------------------------------------------
+
+ private static final class StubRelOptTable implements RelOptTable {
+ private final ImmutableList qualifiedName;
+ private final RelDataType rowType;
+ private final SqlTypeFactoryImpl tf;
+
+ StubRelOptTable(ImmutableList qualifiedName, RelDataType rowType,
+ SqlTypeFactoryImpl tf) {
+ this.qualifiedName = qualifiedName;
+ this.rowType = rowType;
+ this.tf = tf;
+ }
+
+ @Override public List getQualifiedName() { return qualifiedName; }
+ @Override public double getRowCount() { return 100; }
+ @Override public RelDataType getRowType() { return rowType; }
+ @Override public RelOptSchema getRelOptSchema() { return null; }
+ @Override public RelNode toRel(ToRelContext context) {
+ return LogicalTableScan.create(context.getCluster(), this, Collections.emptyList());
+ }
+ @Override public List getCollationList() { return ImmutableList.of(); }
+ @Override public RelDistribution getDistribution() { return null; }
+ @Override public boolean isKey(ImmutableBitSet columns) { return false; }
+ @Override public List getKeys() { return ImmutableList.of(); }
+ @Override public List getReferentialConstraints() {
+ return ImmutableList.of();
+ }
+ @Override public Expression getExpression(Class clazz) { return null; }
+ @Override public RelOptTable extend(List extendedFields) {
+ RelDataTypeFactory.Builder b = tf.builder();
+ for (RelDataTypeField f : rowType.getFieldList()) { b.add(f.getName(), f.getType()); }
+ for (RelDataTypeField f : extendedFields) { b.add(f.getName(), f.getType()); }
+ return new StubRelOptTable(qualifiedName, b.build(), tf);
+ }
+ @Override public List getColumnStrategies() {
+ ImmutableList.Builder b = ImmutableList.builder();
+ for (int i = 0; i < rowType.getFieldCount(); i++) { b.add(ColumnStrategy.NULLABLE); }
+ return b.build();
+ }
+ @Override public C unwrap(Class aClass) {
+ if (aClass.isInstance(this)) { return aClass.cast(this); }
+ return null;
+ }
+ }
+}
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/security/AccessAuthorizerManagerTest.java b/exec/java-exec/src/test/java/org/apache/drill/exec/security/AccessAuthorizerManagerTest.java
new file mode 100644
index 00000000000..05125b4a278
--- /dev/null
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/security/AccessAuthorizerManagerTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.drill.exec.security;
+
+import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.exec.ExecConstants;
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.test.BaseTest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Properties;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for {@link AccessAuthorizerManager}.
+ *
+ * Uses the {@link TestAccessAuthorizerFactory} (name "test", registered via
+ * the test META-INF/services file) to verify ServiceLoader-based factory
+ * discovery and config flattening without requiring the production Ranger
+ * shim's {@code RangerPluginClassLoader} infrastructure.
+ */
+public class AccessAuthorizerManagerTest extends BaseTest {
+
+ /**
+ * Resets the {@code instance} singleton before/after each test so that the
+ * double-checked locking in {@link AccessAuthorizerManager#getAuthorizer}
+ * re-runs the initialization path. {@code reset()} is package-private;
+ * this test lives in the same package.
+ */
+ @Before
+ @After
+ public void resetManagerInstance() {
+ AccessAuthorizerManager.reset();
+ TestAccessAuthorizer.reset();
+ }
+
+ // ========================================================================
+ // Disabled → AllowAll (fail-open)
+ // ========================================================================
+
+ @Test
+ public void getAuthorizer_returnsAllowAll_whenAuthorizerConfigAbsent() {
+ // No drill.exec.security.authorizer property at all → treated as disabled
+ DrillConfig config = DrillConfig.forClient();
+ AccessAuthorizer authorizer = AccessAuthorizerManager.getAuthorizer(config);
+ assertTrue("Expected AllowAllAccessAuthorizer when authorizer config absent",
+ authorizer instanceof AllowAllAccessAuthorizer);
+ }
+
+ @Test
+ public void getAuthorizer_returnsAllowAll_whenEnabledFalse() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "false");
+ DrillConfig config = DrillConfig.create(props);
+ AccessAuthorizer authorizer = AccessAuthorizerManager.getAuthorizer(config);
+ assertTrue("Expected AllowAllAccessAuthorizer when enabled=false",
+ authorizer instanceof AllowAllAccessAuthorizer);
+ }
+
+ @Test
+ public void getAuthorizer_returnsCachedInstance() {
+ DrillConfig config = DrillConfig.forClient();
+ AccessAuthorizer first = AccessAuthorizerManager.getAuthorizer(config);
+ AccessAuthorizer second = AccessAuthorizerManager.getAuthorizer(config);
+ assertSame("Singleton must cache the same instance", first, second);
+ }
+
+ // ========================================================================
+ // Factory discovery via ServiceLoader (test factory, name="test")
+ // ========================================================================
+
+ @Test
+ public void getAuthorizer_selectsFactoryByConfiguredName() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ DrillConfig config = DrillConfig.create(props);
+
+ AccessAuthorizer authorizer = AccessAuthorizerManager.getAuthorizer(config);
+ assertTrue(authorizer instanceof TestAccessAuthorizer);
+ }
+
+ /**
+ * The flattened config passed to the factory must NOT contain the
+ * engine-managed selection keys ("enabled", "name") — only the
+ * plugin-specific remainder of the subtree.
+ */
+ @Test
+ public void getAuthorizer_passesFlattenedConfigWithoutSelectionKeys() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ props.setProperty("drill.exec.security.authorizer.service.name", "myDrillSvc");
+ props.setProperty("drill.exec.security.authorizer.custom.key", "customValue");
+ DrillConfig config = DrillConfig.create(props);
+
+ AccessAuthorizerManager.getAuthorizer(config);
+
+ assertEquals("myDrillSvc", TestAccessAuthorizer.getLastConfig().get("service.name"));
+ assertEquals("customValue", TestAccessAuthorizer.getLastConfig().get("custom.key"));
+ assertFalse("enabled must not leak into factory config",
+ TestAccessAuthorizer.getLastConfig().containsKey("enabled"));
+ assertFalse("name must not leak into factory config",
+ TestAccessAuthorizer.getLastConfig().containsKey("name"));
+ }
+
+ @Test
+ public void getAuthorizer_usesDefaultServiceName_whenServiceNameAbsent() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ DrillConfig config = DrillConfig.create(props);
+
+ AccessAuthorizerManager.getAuthorizer(config);
+ // The test factory applies the same default as the Ranger shim: "drill"
+ assertEquals("drill", TestAccessAuthorizer.getLastServiceName());
+ }
+
+ @Test
+ public void getAuthorizer_forwardsServiceName_whenConfigured() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ props.setProperty("drill.exec.security.authorizer.service.name", "myDrillSvc");
+ DrillConfig config = DrillConfig.create(props);
+
+ AccessAuthorizerManager.getAuthorizer(config);
+ assertEquals("myDrillSvc", TestAccessAuthorizer.getLastServiceName());
+ }
+
+ // ========================================================================
+ // Failure modes (fail-closed)
+ // ========================================================================
+
+ @Test
+ public void getAuthorizer_throws_whenNoFactoryMatchesName() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "nonexistent");
+ DrillConfig config = DrillConfig.create(props);
+
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> AccessAuthorizerManager.getAuthorizer(config));
+ assertTrue(ex.getMessage().contains("nonexistent"));
+ }
+
+ @Test
+ public void getAuthorizer_throws_whenFactoryCreateFails() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ DrillConfig config = DrillConfig.create(props);
+
+ TestAccessAuthorizer.setShouldThrow(true);
+ RuntimeException ex = assertThrows(RuntimeException.class,
+ () -> AccessAuthorizerManager.getAuthorizer(config));
+ assertTrue(ex.getMessage().contains("create boom"));
+ }
+
+ /**
+ * Sanity check that distinct configs (disabled vs enabled) produce
+ * non-identical instances after a reset. This guards against the singleton
+ * cache leaking across tests when @Before/@After reset is misconfigured.
+ */
+ @Test
+ public void getAuthorizer_reinitializesAfterReset() {
+ DrillConfig disabledConfig = DrillConfig.forClient();
+ AccessAuthorizer first = AccessAuthorizerManager.getAuthorizer(disabledConfig);
+ assertTrue(first instanceof AllowAllAccessAuthorizer);
+
+ // Reset and ask for an enabled config — must NOT return the cached allow-all
+ AccessAuthorizerManager.reset();
+ TestAccessAuthorizer.reset();
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ DrillConfig enabledConfig = DrillConfig.create(props);
+
+ AccessAuthorizer second = AccessAuthorizerManager.getAuthorizer(enabledConfig);
+ assertNotSame(first, second);
+ assertTrue(second instanceof TestAccessAuthorizer);
+ }
+
+ // ========================================================================
+ // Shutdown / close lifecycle
+ // ========================================================================
+
+ @Test
+ public void close_withoutInit_isSafe() {
+ // No authorizer cached yet — close() must be a safe no-op.
+ AccessAuthorizerManager.close();
+ }
+
+ @Test
+ public void close_closesCachedAuthorizer_andClearsSingleton() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ DrillConfig config = DrillConfig.create(props);
+
+ AccessAuthorizer first = AccessAuthorizerManager.getAuthorizer(config);
+ assertTrue(first instanceof TestAccessAuthorizer);
+
+ AccessAuthorizerManager.close();
+
+ assertEquals("close() must be forwarded to the cached authorizer", 1,
+ TestAccessAuthorizer.getCloseCount());
+ AccessAuthorizer second = AccessAuthorizerManager.getAuthorizer(config);
+ assertNotSame("After close() the singleton must be re-creatable", first, second);
+ }
+
+ @Test
+ public void close_isIdempotent_andReinitializesAfterRestart() {
+ // Simulates a Drillbit shutdown + restart inside the same JVM: after
+ // close() (even called twice), a new getAuthorizer() must build a fresh,
+ // working instance — the old one dropped its plugin resources on close.
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ DrillConfig config = DrillConfig.create(props);
+
+ AccessAuthorizer first = AccessAuthorizerManager.getAuthorizer(config);
+ AccessAuthorizerManager.close();
+ AccessAuthorizerManager.close(); // singleton already cleared: safe no-op
+ AccessAuthorizer second = AccessAuthorizerManager.getAuthorizer(config);
+
+ assertEquals("close() must be forwarded exactly once", 1,
+ TestAccessAuthorizer.getCloseCount());
+ assertNotSame(first, second);
+ }
+}
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/security/AllowAllAccessAuthorizerTest.java b/exec/java-exec/src/test/java/org/apache/drill/exec/security/AllowAllAccessAuthorizerTest.java
new file mode 100644
index 00000000000..6620bd688aa
--- /dev/null
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/security/AllowAllAccessAuthorizerTest.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.drill.exec.security;
+
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+import org.apache.drill.test.BaseTest;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for {@link AllowAllAccessAuthorizer}.
+ * Verifies the fail-open behavior used when the access authorizer is
+ * disabled or unavailable.
+ */
+public class AllowAllAccessAuthorizerTest extends BaseTest {
+
+ private final AllowAllAccessAuthorizer authorizer = new AllowAllAccessAuthorizer();
+
+ @Test
+ public void checkTableAccess_returnsTrue() {
+ assertTrue(authorizer.checkTableAccess(
+ UserIdentity.of("alice"), "mysql", "shf", "orders", AccessType.SELECT));
+ assertTrue(authorizer.checkTableAccess(
+ UserIdentity.of("bob"), "dfs", "tmp", "foo", AccessType.CREATE));
+ }
+
+ @Test
+ public void checkColumnAccess_returnsTrue() {
+ Set columns = new HashSet<>(Arrays.asList("id", "amount"));
+ assertTrue(authorizer.checkColumnAccess(
+ UserIdentity.of("alice"), "mysql", "shf", "orders", columns, AccessType.SELECT));
+ // Empty column set must still pass (fail-open)
+ assertTrue(authorizer.checkColumnAccess(
+ UserIdentity.of("alice"), "mysql", "shf", "orders", Collections.emptySet(), AccessType.SELECT));
+ }
+
+ @Test
+ public void checkTableAccess_returnsTrue_withNullArgs() {
+ // Fail-open contract: even null inputs must not throw; method returns true
+ assertTrue(authorizer.checkTableAccess(null, null, null, null, null));
+ }
+}
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/security/DdlAccessCheckerTest.java b/exec/java-exec/src/test/java/org/apache/drill/exec/security/DdlAccessCheckerTest.java
new file mode 100644
index 00000000000..b932c6ccbe9
--- /dev/null
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/security/DdlAccessCheckerTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.drill.exec.security;
+
+import java.util.Properties;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.apache.drill.common.config.DrillConfig;
+import org.apache.drill.common.exceptions.UserException;
+import org.apache.drill.exec.ExecConstants;
+import org.apache.drill.exec.ops.QueryContext;
+import org.apache.drill.exec.proto.UserBitShared;
+import org.apache.drill.exec.rpc.user.UserSession;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.store.AbstractSchema;
+import org.apache.drill.test.BaseTest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link DdlAccessChecker}. Uses the {@link TestAccessAuthorizer}
+ * (factory "test", registered via the test META-INF/services file) loaded
+ * through {@link AccessAuthorizerManager} with a real {@link DrillConfig};
+ * {@link QueryContext} and {@link AbstractSchema} are Mockito mocks.
+ */
+public class DdlAccessCheckerTest extends BaseTest {
+
+ private QueryContext context;
+ private AbstractSchema schema;
+
+ @Before
+ @After
+ public void reset() {
+ AccessAuthorizerManager.reset();
+ TestAccessAuthorizer.reset();
+ }
+
+ private void initContext(DrillConfig config, String userName) {
+ UserSession session = mock(UserSession.class);
+ when(session.getCredentials())
+ .thenReturn(UserBitShared.UserCredentials.newBuilder().setUserName(userName).build());
+ context = mock(QueryContext.class);
+ when(context.getSession()).thenReturn(session);
+ when(context.getConfig()).thenReturn(config);
+ schema = mock(AbstractSchema.class);
+ }
+
+ private DrillConfig enabledConfig() {
+ Properties props = new Properties();
+ props.setProperty(ExecConstants.AUTHORIZER_ENABLED, "true");
+ props.setProperty(ExecConstants.AUTHORIZER_NAME, "test");
+ return DrillConfig.create(props);
+ }
+
+ @Test
+ public void allowsAndMapsResourceWhenNothingDenied() {
+ initContext(enabledConfig(), "alice");
+ when(schema.getSchemaPath()).thenReturn(ImmutableList.of("dfs", "tmp"));
+
+ DdlAccessChecker.checkDdlAccess(context, schema, "t1", AccessType.CREATE);
+
+ TestAccessAuthorizer.TableCheck check = TestAccessAuthorizer.getLastTableCheck();
+ assertNotNull(check);
+ assertEquals("alice", check.user);
+ assertEquals("dfs", check.dataSource);
+ assertEquals("tmp", check.schema);
+ assertEquals("t1", check.table);
+ assertEquals(AccessType.CREATE, check.accessType);
+ }
+
+ @Test
+ public void throwsPermissionErrorWhenCreateDenied() {
+ initContext(enabledConfig(), "bob");
+ when(schema.getSchemaPath()).thenReturn(ImmutableList.of("dfs", "tmp"));
+ TestAccessAuthorizer.setDeniedAccessTypes(ImmutableSet.of(AccessType.CREATE));
+
+ UserException e = assertThrows(UserException.class,
+ () -> DdlAccessChecker.checkDdlAccess(context, schema, "t1", AccessType.CREATE));
+
+ assertEquals(UserBitShared.DrillPBError.ErrorType.PERMISSION, e.getErrorType());
+ assertTrue(e.getOriginalMessage().contains("lacks CREATE privilege"));
+ assertTrue(e.getOriginalMessage().contains("dfs.tmp.t1"));
+ }
+
+ @Test
+ public void denyIsExactPerAccessType() {
+ initContext(enabledConfig(), "bob");
+ when(schema.getSchemaPath()).thenReturn(ImmutableList.of("dfs", "tmp"));
+ TestAccessAuthorizer.setDeniedAccessTypes(ImmutableSet.of(AccessType.DROP));
+
+ // DROP is denied, CREATE is not: the CREATE check must pass.
+ DdlAccessChecker.checkDdlAccess(context, schema, "t1", AccessType.CREATE);
+
+ UserException e = assertThrows(UserException.class,
+ () -> DdlAccessChecker.checkDdlAccess(context, schema, "t1", AccessType.DROP));
+ assertEquals(UserBitShared.DrillPBError.ErrorType.PERMISSION, e.getErrorType());
+ assertTrue(e.getOriginalMessage().contains("lacks DROP privilege"));
+ }
+
+ @Test
+ public void mapsSchemaLessQualifiedNameToDefaultSchema() {
+ initContext(enabledConfig(), "alice");
+ when(schema.getSchemaPath()).thenReturn(ImmutableList.of("dfs"));
+
+ DdlAccessChecker.checkDdlAccess(context, schema, "t", AccessType.CREATE);
+
+ TestAccessAuthorizer.TableCheck check = TestAccessAuthorizer.getLastTableCheck();
+ assertNotNull(check);
+ assertEquals("dfs", check.dataSource);
+ assertEquals("default", check.schema);
+ assertEquals("t", check.table);
+ }
+
+ @Test
+ public void allowsAllWhenAuthorizationDisabled() {
+ initContext(DrillConfig.forClient(), "alice");
+ when(schema.getSchemaPath()).thenReturn(ImmutableList.of("dfs", "tmp"));
+ // deny rules are irrelevant: the manager returns the allow-all authorizer
+ TestAccessAuthorizer.setDeniedAccessTypes(ImmutableSet.of(AccessType.CREATE));
+
+ DdlAccessChecker.checkDdlAccess(context, schema, "t1", AccessType.CREATE);
+
+ // The test authorizer was never consulted.
+ assertNull(TestAccessAuthorizer.getLastTableCheck());
+ }
+}
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/security/TableAccessResourceTest.java b/exec/java-exec/src/test/java/org/apache/drill/exec/security/TableAccessResourceTest.java
new file mode 100644
index 00000000000..9944de0a935
--- /dev/null
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/security/TableAccessResourceTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.drill.exec.security;
+
+import org.apache.drill.test.BaseTest;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+/**
+ * Unit tests for {@link TableAccessResource#resolve(List)}: the single
+ * mapping from a Calcite qualified name to the (datasource, schema, table)
+ * access-check resource. Both table-level (DrillCalciteCatalogReader) and
+ * column-level (ColumnAccessChecker) checks go through it, so these tests
+ * pin the exact resource each check will address.
+ */
+public class TableAccessResourceTest extends BaseTest {
+
+ // ========================================================================
+ // Three or more segments: datasource.schema.table
+ // ========================================================================
+
+ @Test
+ public void resolve_threeSegments_splitsDataSourceSchemaTable() {
+ TableAccessResource r = TableAccessResource.resolve(Arrays.asList("mysql", "shf", "orders"));
+ assertEquals("mysql", r.getDataSource());
+ assertEquals("shf", r.getSchemaPath());
+ assertEquals("orders", r.getTable());
+ }
+
+ @Test
+ public void resolve_nestedSchema_joinsMiddleSegments() {
+ // Nested schema path: [dfs, tmp, sub, orders] -> schema "tmp.sub"
+ TableAccessResource r = TableAccessResource.resolve(Arrays.asList("dfs", "tmp", "sub", "orders"));
+ assertEquals("dfs", r.getDataSource());
+ assertEquals("tmp.sub", r.getSchemaPath());
+ assertEquals("orders", r.getTable());
+ }
+
+ @Test
+ public void resolve_schema_excludesDataSourcePrefix() {
+ // The schema MUST NOT include the datasource prefix, otherwise policy
+ // matching fails (policy has schema=shf but request would send mysql.shf).
+ TableAccessResource r = TableAccessResource.resolve(Arrays.asList("mysql", "shf", "orders"));
+ assertEquals("shf", r.getSchemaPath());
+ }
+
+ // ========================================================================
+ // Two segments: datasource.table (backend without schema concept)
+ // ========================================================================
+
+ @Test
+ public void resolve_twoSegments_synthesizesDefaultSchema_forDfs() {
+ TableAccessResource r = TableAccessResource.resolve(Arrays.asList("dfs", "orders"));
+ assertEquals("dfs", r.getDataSource());
+ assertEquals("default", r.getSchemaPath());
+ assertEquals("orders", r.getTable());
+ }
+
+ @Test
+ public void resolve_twoSegments_synthesizesDefaultSchema_forCp() {
+ TableAccessResource r = TableAccessResource.resolve(Arrays.asList("cp", "employee.json"));
+ assertEquals("cp", r.getDataSource());
+ assertEquals("default", r.getSchemaPath());
+ }
+
+ @Test
+ public void resolve_twoSegments_defaultSchemaIsCaseInsensitive() {
+ // "DFS" lowercases to "dfs" which hits the explicit case -> "default"
+ assertEquals("default",
+ TableAccessResource.resolve(Arrays.asList("DFS", "orders")).getSchemaPath());
+ }
+
+ @Test
+ public void resolve_twoSegments_unknownPlugin_usesPluginNameAsSchema() {
+ // No explicit default-schema mapping for "mysql": the plugin name itself
+ // becomes the default schema namespace (preserving case).
+ TableAccessResource r = TableAccessResource.resolve(Arrays.asList("mysql", "orders"));
+ assertEquals("mysql", r.getDataSource());
+ assertEquals("mysql", r.getSchemaPath());
+ assertEquals("MySql",
+ TableAccessResource.resolve(Arrays.asList("MySql", "orders")).getSchemaPath());
+ }
+
+ // ========================================================================
+ // Single segment: root-level table
+ // ========================================================================
+
+ @Test
+ public void resolve_singleSegment_usesTableAsDataSourceNamespace() {
+ TableAccessResource r = TableAccessResource.resolve(Collections.singletonList("orders"));
+ assertEquals("orders", r.getDataSource());
+ assertEquals("orders", r.getSchemaPath());
+ assertEquals("orders", r.getTable());
+ }
+
+ // ========================================================================
+ // Invalid input
+ // ========================================================================
+
+ @Test
+ public void resolve_nullQualifiedName_throws() {
+ assertThrows(NullPointerException.class, () -> TableAccessResource.resolve(null));
+ }
+
+ @Test
+ public void resolve_emptyQualifiedName_throws() {
+ List empty = Collections.emptyList();
+ assertThrows(IllegalArgumentException.class, () -> TableAccessResource.resolve(empty));
+ }
+
+ // ========================================================================
+ // toString: used in permission-error messages
+ // ========================================================================
+
+ @Test
+ public void toString_rendersDottedPath() {
+ TableAccessResource r = TableAccessResource.resolve(Arrays.asList("mysql", "shf", "orders"));
+ assertEquals("mysql.shf.orders", r.toString());
+ }
+}
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/security/TestAccessAuthorizer.java b/exec/java-exec/src/test/java/org/apache/drill/exec/security/TestAccessAuthorizer.java
new file mode 100644
index 00000000000..51459737ab0
--- /dev/null
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/security/TestAccessAuthorizer.java
@@ -0,0 +1,161 @@
+/*
+ * 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.drill.exec.security;
+
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessType;
+import org.apache.drill.exec.security.spi.UserIdentity;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Test-only {@link AccessAuthorizer} created by
+ * {@link TestAccessAuthorizerFactory}. Records the flattened config map it
+ * was created with so tests can assert the manager's config handling.
+ *
+ * Also provides test knobs for authorization checks:
+ *
+ * - {@code setDeniedAccessTypes(...)} — denies exactly the given access
+ * types (default: deny nothing, i.e. allow all);
+ * - {@code getLastTableCheck()} / {@code getLastColumnCheck()} — records
+ * the most recent check arguments so tests can assert resource mapping
+ * (datasource/schema/table and the requesting user).
+ *
+ * All state is static so a single instance created via the manager serves a
+ * whole embedded Drillbit; call {@link #reset()} between tests.
+ */
+public class TestAccessAuthorizer implements AccessAuthorizer {
+
+ /** Arguments of the most recent checkTableAccess call. */
+ public static class TableCheck {
+ public final String user;
+ public final String dataSource;
+ public final String schema;
+ public final String table;
+ public final AccessType accessType;
+
+ TableCheck(String user, String dataSource, String schema, String table, AccessType accessType) {
+ this.user = user;
+ this.dataSource = dataSource;
+ this.schema = schema;
+ this.table = table;
+ this.accessType = accessType;
+ }
+ }
+
+ /** Arguments of the most recent checkColumnAccess call. */
+ public static class ColumnCheck {
+ public final String user;
+ public final String dataSource;
+ public final String schema;
+ public final String table;
+ public final Set columns;
+ public final AccessType accessType;
+
+ ColumnCheck(String user, String dataSource, String schema, String table,
+ Set columns, AccessType accessType) {
+ this.user = user;
+ this.dataSource = dataSource;
+ this.schema = schema;
+ this.table = table;
+ this.columns = columns;
+ this.accessType = accessType;
+ }
+ }
+
+ private static volatile boolean shouldThrow;
+ private static volatile String lastServiceName;
+ private static volatile Map lastConfig;
+ private static volatile Set deniedAccessTypes = Collections.emptySet();
+ private static volatile TableCheck lastTableCheck;
+ private static volatile ColumnCheck lastColumnCheck;
+ private static volatile int closeCount;
+
+ public static void reset() {
+ shouldThrow = false;
+ lastServiceName = null;
+ lastConfig = null;
+ deniedAccessTypes = Collections.emptySet();
+ lastTableCheck = null;
+ lastColumnCheck = null;
+ closeCount = 0;
+ }
+
+ public static String getLastServiceName() {
+ return lastServiceName;
+ }
+
+ public static Map getLastConfig() {
+ return lastConfig;
+ }
+
+ public static void setShouldThrow(boolean value) {
+ shouldThrow = value;
+ }
+
+ /**
+ * Sets the access types that will be denied. Any access type not in the set
+ * is allowed. Default (after {@link #reset()}) is an empty set: allow all.
+ */
+ public static void setDeniedAccessTypes(Set types) {
+ deniedAccessTypes = types == null ? Collections.emptySet() : types;
+ }
+
+ /** Returns the most recent checkTableAccess call, or {@code null} if none. */
+ public static TableCheck getLastTableCheck() {
+ return lastTableCheck;
+ }
+
+ /** Returns the most recent checkColumnAccess call, or {@code null} if none. */
+ public static ColumnCheck getLastColumnCheck() {
+ return lastColumnCheck;
+ }
+
+ /** Returns how many times close() has been invoked (shutdown lifecycle). */
+ public static int getCloseCount() {
+ return closeCount;
+ }
+
+ TestAccessAuthorizer(Map config) {
+ lastConfig = config;
+ lastServiceName = config.getOrDefault("service.name", "drill");
+ if (shouldThrow) {
+ throw new RuntimeException("create boom");
+ }
+ }
+
+ @Override
+ public boolean checkTableAccess(UserIdentity user, String dataSource, String schema,
+ String table, AccessType accessType) {
+ lastTableCheck = new TableCheck(user.getUser(), dataSource, schema, table, accessType);
+ return !deniedAccessTypes.contains(accessType);
+ }
+
+ @Override
+ public boolean checkColumnAccess(UserIdentity user, String dataSource, String schema,
+ String table, Set columns, AccessType accessType) {
+ lastColumnCheck = new ColumnCheck(user.getUser(), dataSource, schema, table, columns, accessType);
+ return !deniedAccessTypes.contains(accessType);
+ }
+
+ @Override
+ public void close() {
+ closeCount++;
+ }
+}
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/security/TestAccessAuthorizerFactory.java b/exec/java-exec/src/test/java/org/apache/drill/exec/security/TestAccessAuthorizerFactory.java
new file mode 100644
index 00000000000..47b222965a0
--- /dev/null
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/security/TestAccessAuthorizerFactory.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.drill.exec.security;
+
+import org.apache.drill.exec.security.spi.AccessAuthorizer;
+import org.apache.drill.exec.security.spi.AccessAuthorizerFactory;
+
+import java.util.Map;
+
+/**
+ * Test-only {@link AccessAuthorizerFactory} registered via the test
+ * {@code META-INF/services} file. Selected by configuring
+ * {@code drill.exec.security.authorizer.name=test}, which lets
+ * {@link AccessAuthorizerManagerTest} exercise the manager's ServiceLoader
+ * discovery and config-flattening logic WITHOUT touching the production
+ * Ranger shim (whose {@code RangerPluginClassLoader} needs the assembled
+ * distribution directories).
+ */
+public class TestAccessAuthorizerFactory implements AccessAuthorizerFactory {
+
+ public static final String NAME = "test";
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public AccessAuthorizer createAuthorizer(Map config) {
+ return new TestAccessAuthorizer(config);
+ }
+}
diff --git a/exec/java-exec/src/test/java/org/apache/drill/exec/sql/TestWithClauseRangerAuthz.java b/exec/java-exec/src/test/java/org/apache/drill/exec/sql/TestWithClauseRangerAuthz.java
new file mode 100644
index 00000000000..a2633361ddb
--- /dev/null
+++ b/exec/java-exec/src/test/java/org/apache/drill/exec/sql/TestWithClauseRangerAuthz.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor 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.drill.exec.sql;
+
+import org.apache.drill.categories.SqlTest;
+import org.apache.drill.exec.proto.UserBitShared;
+import org.apache.drill.test.BaseTestQuery;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+/**
+ * CTE (Common Table Expression, {@code WITH ... AS (...)}) authorization tests
+ * under Ranger column-level access control.
+ *
+ * Prerequisites: These tests require a live Drill cluster with:
+ *
+ * - Ranger authorization enabled ({@code drill.exec.security.authorizer.enabled=true})
+ * - A MySQL storage plugin named {@code mysql} with schema {@code shf}
+ * - Tables {@code mysql.shf.orders} (columns: id, amount, user_id, order_date)
+ * and {@code mysql.shf.users} (all columns)
+ * - Ranger Policy A: {@code users} table, column {@code *}, SELECT
+ * - Ranger Policy B: {@code orders} table, columns {@code id, amount}, SELECT
+ *
+ * See {@code docs/dev/RangerAuthorization.md} section 4.1 for the sample policies.
+ *
+ * Why CTEs don't need special handling: Calcite's
+ * {@code SqlToRelConverter} inlines CTE definitions into the RelNode tree
+ * before {@code ColumnAccessChecker} runs. After inlining, every
+ * {@code TableScan} seen by the {@code RelShuttle} is a real underlying table,
+ * so column-level checks apply uniformly regardless of whether the original
+ * SQL used a CTE or a direct {@code SELECT}.
+ *
+ * Tests are {@code @Ignore}d by default because they depend on external
+ * resources. Remove {@code @Ignore} when running against a configured
+ * Ranger + MySQL environment.
+ */
+@Category(SqlTest.class)
+@Ignore("Requires Ranger authorization enabled with MySQL storage plugin and sample policies")
+public class TestWithClauseRangerAuthz extends BaseTestQuery {
+ //private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(TestWithClauseRangerAuthz.class);
+
+ private static final String ACCESS_DENIED = UserBitShared.DrillPBError.ErrorType.PERMISSION.name();
+
+ /**
+ * DENY: CTE body uses {@code SELECT *} which expands to all columns including
+ * {@code order_date} (not in Policy B).
+ */
+ @Test
+ public void deny_cteSelectStarFromOrders() throws Exception {
+ String query = "WITH t AS (SELECT * FROM mysql.shf.orders)\n" +
+ "SELECT * FROM t";
+ errorMsgTestHelper(query, ACCESS_DENIED);
+ }
+
+ /**
+ * PASS: CTE body only selects {@code id} (in Policy B); outer query selects
+ * from the CTE, which resolves to the same authorized column.
+ */
+ @Test
+ public void pass_cteSelectAuthorizedColumn() throws Exception {
+ String query = "WITH t AS (SELECT id FROM mysql.shf.orders)\n" +
+ "SELECT * FROM t";
+ test(query);
+ }
+
+ /**
+ * DENY: CTE body selects {@code order_date} (not in Policy B). Even though
+ * the outer query projects {@code order_date} from the CTE, the column
+ * access check traces back to the underlying {@code orders} table scan.
+ */
+ @Test
+ public void deny_cteSelectUnauthorizedColumnProjected() throws Exception {
+ String query = "WITH t AS (SELECT id, order_date FROM mysql.shf.orders)\n" +
+ "SELECT order_date FROM t";
+ errorMsgTestHelper(query, ACCESS_DENIED);
+ }
+
+ /**
+ * DENY: The CTE body references {@code order_date} (not in Policy B) even
+ * though the outer query only projects {@code id}. After CTE inlining, the
+ * {@code TableScan} for {@code orders} has both {@code id} and
+ * {@code order_date} referenced, so the check fails on {@code order_date}.
+ */
+ @Test
+ public void deny_cteBodyReferencesUnauthorizedColumnEvenIfOuterDoesNot() throws Exception {
+ String query = "WITH t AS (SELECT id, order_date FROM mysql.shf.orders)\n" +
+ "SELECT id FROM t";
+ errorMsgTestHelper(query, ACCESS_DENIED);
+ }
+
+ /**
+ * PASS: Multiple CTEs referencing different tables. CTE {@code a} selects
+ * {@code id} from {@code orders} (Policy B); CTE {@code b} selects {@code id}
+ * from {@code users} (Policy A via {@code *}). Outer query selects from
+ * {@code b} only. All referenced columns are authorized.
+ */
+ @Test
+ public void pass_multipleCtesDifferentTables() throws Exception {
+ String query = "WITH a AS (SELECT id FROM mysql.shf.orders),\n" +
+ " b AS (SELECT id FROM mysql.shf.users)\n" +
+ "SELECT * FROM b";
+ test(query);
+ }
+
+ // ------------------------------------------------------------------
+ // Table alias tests — verify that column-level authorization is
+ // transparent to table aliases. Calcite resolves aliases during
+ // SqlToRel conversion; RexInputRef indexes point to row-type
+ // positions (not alias names), and RelMetadataQuery.getColumnOrigins
+ // traces through to the underlying TableScan, so ColumnAccessChecker
+ // sees the real table/column regardless of any alias used in SQL.
+ // ------------------------------------------------------------------
+
+ /**
+ * DENY: Table alias on an unauthorized column. {@code o.order_date} resolves
+ * to {@code order_date} of {@code orders} (not in Policy B).
+ */
+ @Test
+ public void deny_simpleAliasUnauthorizedColumn() throws Exception {
+ String query = "SELECT o.order_date FROM mysql.shf.orders o";
+ errorMsgTestHelper(query, ACCESS_DENIED);
+ }
+
+ /**
+ * PASS: Join with aliases on two tables. {@code a.id} resolves to
+ * {@code users.id} (Policy A via {@code *}); {@code b.id} resolves to
+ * {@code orders.id} (Policy B). Join condition columns also authorized.
+ */
+ @Test
+ public void pass_joinWithAliases() throws Exception {
+ String query = "SELECT a.id FROM mysql.shf.users a " +
+ "JOIN mysql.shf.orders b ON a.id = b.id";
+ test(query);
+ }
+
+ /**
+ * PASS: Simple table alias on an authorized column. {@code o.id} resolves
+ * to the {@code id} column of {@code orders} (in Policy B).
+ */
+ @Test
+ public void pass_simpleAliasAuthorizedColumn() throws Exception {
+ String query = "SELECT o.id FROM mysql.shf.orders o";
+ test(query);
+ }
+
+ /**
+ * PASS: Alias used in WHERE clause on an authorized column.
+ * {@code o.amount} resolves to {@code orders.amount} (in Policy B).
+ */
+ @Test
+ public void pass_aliasInWhereAuthorizedColumn() throws Exception {
+ String query = "SELECT o.id FROM mysql.shf.orders o WHERE o.amount = '150.0'";
+ test(query);
+ }
+}
diff --git a/exec/java-exec/src/test/resources/META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory b/exec/java-exec/src/test/resources/META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory
new file mode 100644
index 00000000000..220e6b84e0c
--- /dev/null
+++ b/exec/java-exec/src/test/resources/META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory
@@ -0,0 +1,15 @@
+# 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.
+org.apache.drill.exec.security.TestAccessAuthorizerFactory
\ No newline at end of file
diff --git a/exec/java-exec/src/test/resources/core-site.xml b/exec/java-exec/src/test/resources/core-site.xml
index 0392da89148..0dc5b65eafa 100644
--- a/exec/java-exec/src/test/resources/core-site.xml
+++ b/exec/java-exec/src/test/resources/core-site.xml
@@ -51,4 +51,12 @@
datanucleus.autoCreateTables
true
+
+ io.compression.codecs
+ org.apache.hadoop.io.compress.DefaultCodec,org.apache.hadoop.io.compress.GzipCodec,org.apache.hadoop.io.compress.BZip2Codec,org.apache.hadoop.io.compress.SnappyCodec,org.apache.hadoop.io.compress.Lz4Codec,org.apache.hadoop.io.compress.ZStandardCodec
+ Explicit codec list: skips ServiceLoader discovery, which
+ fails on Windows when the BrotliCodec native library (brotli.dll) is
+ not available. BrotliCodec's constructor eagerly loads the native
+ library and throws ServiceConfigurationError otherwise.
+
\ No newline at end of file
diff --git a/exec/pom.xml b/exec/pom.xml
index 29b0ccf87e7..68f729abfd3 100644
--- a/exec/pom.xml
+++ b/exec/pom.xml
@@ -50,6 +50,7 @@
memory
rpc
vector
+ security-spi
java-exec
jdbc
diff --git a/exec/security-spi/pom.xml b/exec/security-spi/pom.xml
new file mode 100644
index 00000000000..c07a6e7dd84
--- /dev/null
+++ b/exec/security-spi/pom.xml
@@ -0,0 +1,42 @@
+
+
+
+ 4.0.0
+
+ exec-parent
+ org.apache.drill.exec
+ 1.23.0-SNAPSHOT
+
+
+ org.apache.drill
+ drill-security-spi
+ Drill : Exec : Security SPI
+
+ Access authorization SPI for Drill (AccessAuthorizer,
+ AccessAuthorizerFactory, AccessType, UserIdentity). Depends only on
+ the JDK so external authorization plugins (e.g. Apache Ranger) can
+ implement the SPI without a Drill engine dependency — analogous to
+ Presto's presto-spi module.
+
+
diff --git a/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessAuthorizer.java b/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessAuthorizer.java
new file mode 100644
index 00000000000..1b82fb457e8
--- /dev/null
+++ b/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessAuthorizer.java
@@ -0,0 +1,96 @@
+/*
+ * 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.drill.exec.security.spi;
+
+import java.util.Set;
+
+/**
+ * Drill access authorization SPI interface.
+ *
+ * Mirrors Presto's {@code SystemAccessControl}: the engine (via
+ * {@code AccessAuthorizerManager}) discovers an {@link AccessAuthorizerFactory}
+ * through {@code ServiceLoader} and calls {@code factory.createAuthorizer(config)} to
+ * obtain a fully-initialized instance. Implementations must complete all
+ * initialization in the factory/constructor phase; there is no separate
+ * {@code init()} lifecycle method.
+ *
+ * This interface and its parameter types ({@link UserIdentity}, strings,
+ * sets) depend only on the JDK, so implementations can live outside the
+ * Drill engine (e.g. an Apache Ranger plugin) with no dependency beyond
+ * this small SPI module — analogous to Presto's {@code presto-spi}.
+ *
+ * There is deliberately no {@code isEnabled()} method: an instance that
+ * exists is initialized and active (the factory completes initialization or
+ * throws, fail-closed). Whether authorization is enabled at all is an
+ * engine-side configuration concern — the engine selects this SPI or its own
+ * allow-all implementation (mirroring Presto's
+ * {@code SystemAccessControl} / {@code AllowAllAccessControl}).
+ *
+ * Access types are identified by the {@link AccessType} enum (mirroring
+ * Presto's {@code Privilege}). Callers pass the constant to
+ * {@link #checkTableAccess} or {@link #checkColumnAccess}. This avoids a
+ * dedicated method per access type and keeps the interface stable as new
+ * operations are added.
+ */
+public interface AccessAuthorizer {
+
+ /**
+ * Checks table-level access permission.
+ *
+ * @param user the querying user identity
+ * @param dataSource the data source name (StoragePlugin name, e.g. "dfs")
+ * @param schema the schema path (e.g. "dfs.tmp")
+ * @param table the table name
+ * @param accessType the access type (e.g. {@link AccessType#SELECT},
+ * {@link AccessType#CREATE})
+ * @return {@code true} if access is allowed
+ */
+ boolean checkTableAccess(UserIdentity user, String dataSource, String schema,
+ String table, AccessType accessType);
+
+ /**
+ * Checks column-level access permission for a set of columns. Returns
+ * {@code true} only if the user has the specified access type on ALL given
+ * columns.
+ *
+ * @param user the querying user identity
+ * @param dataSource the data source name (StoragePlugin name, e.g. "dfs")
+ * @param schema the schema path (e.g. "dfs.tmp")
+ * @param table the table name
+ * @param columns the set of column names being accessed
+ * @param accessType the access type (e.g. {@link AccessType#SELECT})
+ * @return {@code true} if access is allowed for every column
+ */
+ boolean checkColumnAccess(UserIdentity user, String dataSource, String schema,
+ String table, Set columns, AccessType accessType);
+
+ /**
+ * Releases any resources held by this authorizer (background policy-refresh
+ * threads, caches, open connections). Invoked by the engine when the
+ * Drillbit shuts down; after this call the instance must be considered
+ * unusable, and the engine drops its cached reference so a subsequent
+ * Drillbit start initializes a fresh instance.
+ *
+ * Default no-op: implementations that hold no resources do not need to
+ * override. Implementations should be idempotent and must not throw checked
+ * exceptions — the engine logs and ignores close failures rather than
+ * aborting shutdown.
+ */
+ default void close() {
+ // no-op by default
+ }
+}
diff --git a/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessAuthorizerFactory.java b/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessAuthorizerFactory.java
new file mode 100644
index 00000000000..9b3f1c438f8
--- /dev/null
+++ b/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessAuthorizerFactory.java
@@ -0,0 +1,54 @@
+/*
+ * 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.drill.exec.security.spi;
+
+import java.util.Map;
+
+/**
+ * Factory SPI for creating {@link AccessAuthorizer} instances.
+ *
+ * Mirrors Presto's {@code SystemAccessControlFactory}. Implementations are
+ * discovered via {@code ServiceLoader} (registered under
+ * {@code META-INF/services/org.apache.drill.exec.security.spi.AccessAuthorizerFactory})
+ * and selected by the {@code drill.exec.security.authorizer.name} configuration
+ * key matching {@link #getName()}.
+ *
+ * The {@code config} map carries the flattened properties of the
+ * {@code drill.exec.security.authorizer} configuration subtree (with
+ * {@code enabled} and {@code name} already removed). It contains only JDK
+ * types so implementations never depend on Drill engine configuration
+ * classes — this keeps an authorization plugin (e.g. the Ranger plugin)
+ * portable to external repositories.
+ */
+public interface AccessAuthorizerFactory {
+
+ /**
+ * @return the factory name matched against
+ * {@code drill.exec.security.authorizer.name} (e.g. "ranger")
+ */
+ String getName();
+
+ /**
+ * Creates a fully-initialized {@link AccessAuthorizer}. All initialization
+ * (classloader setup, policy engine bootstrap, ...) must complete here or
+ * by throwing — there is no separate init lifecycle on the authorizer.
+ *
+ * @param config flattened authorizer configuration properties; never {@code null}
+ * @return a ready-to-use authorizer instance
+ */
+ AccessAuthorizer createAuthorizer(Map config);
+}
diff --git a/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessType.java b/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessType.java
new file mode 100644
index 00000000000..9962793c8fe
--- /dev/null
+++ b/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/AccessType.java
@@ -0,0 +1,41 @@
+/*
+ * 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.drill.exec.security.spi;
+
+/**
+ * Access type for {@link AccessAuthorizer#checkTableAccess} and
+ * {@link AccessAuthorizer#checkColumnAccess} — mirrors Presto's
+ * {@code io.prestosql.spi.security.Privilege} enum.
+ *
+ * The enum keeps the set of access types a closed, compile-time-checked
+ * vocabulary: a typo like {@code "SELEC"} is a compile error instead of a
+ * run-time denial. Adding a constant is binary-compatible (implementations
+ * keep working; unknown values are denied fail-closed by implementations
+ * such as the Ranger plugin, which maps this enum to its own
+ * {@code DrillAccessType} by name).
+ *
+ * The Drill engine issues {@link #SELECT} for table- and column-level
+ * checks during SQL validation, and {@link #CREATE} / {@link #DROP} for DDL
+ * authorization (CREATE TABLE / CTAS, CREATE VIEW from the DDL handlers;
+ * DROP TABLE / DROP VIEW from their handlers). Temporary tables are
+ * session-scoped and bypass these checks.
+ */
+public enum AccessType {
+ SELECT,
+ CREATE,
+ DROP
+}
diff --git a/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/UserIdentity.java b/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/UserIdentity.java
new file mode 100644
index 00000000000..70e1edd348d
--- /dev/null
+++ b/exec/security-spi/src/main/java/org/apache/drill/exec/security/spi/UserIdentity.java
@@ -0,0 +1,117 @@
+/*
+ * 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.drill.exec.security.spi;
+
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * The identity of a querying user, as seen by authorization plugins.
+ * Carries the user name, optional group names (resolved by the engine at
+ * authentication time) and an optional {@link Principal}.
+ */
+public final class UserIdentity {
+
+ private final String user;
+ private final Set groups;
+ private final Optional principal;
+
+ private UserIdentity(String user, Set groups,
+ Optional principal) {
+ this.user = Objects.requireNonNull(user, "user is null");
+ this.groups = groups == null ? Collections.emptySet() : Collections.unmodifiableSet(groups);
+ this.principal = principal == null ? Optional.empty() : principal;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public Set getGroups() {
+ return groups;
+ }
+
+ public Optional getPrincipal() {
+ return principal;
+ }
+
+ // ---------- Builder ----------
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Convenience factory for a user identity with only a user name
+ * (no groups, no principal). Used by engine mount points that only
+ * have the authenticated user name available.
+ */
+ public static UserIdentity of(String user) {
+ return builder().setUser(user).build();
+ }
+
+ public static class Builder {
+ private String user;
+ private Set groups;
+ private Optional principal = Optional.empty();
+
+ public Builder setUser(String user) {
+ this.user = user;
+ return this;
+ }
+
+ public Builder setGroups(Set groups) {
+ this.groups = groups;
+ return this;
+ }
+
+ public Builder setPrincipal(Optional principal) {
+ this.principal = principal;
+ return this;
+ }
+
+ public UserIdentity build() {
+ return new UserIdentity(user, groups, principal);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ UserIdentity that = (UserIdentity) o;
+ return user.equals(that.user) &&
+ groups.equals(that.groups) &&
+ principal.equals(that.principal);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(user, groups, principal);
+ }
+
+ @Override
+ public String toString() {
+ return "UserIdentity{user='" + user + "', groups=" + groups + "}";
+ }
+}
diff --git a/pom.xml b/pom.xml
index 1a107272670..5013bd13cb1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,44 @@
2.2
2.6.1-hadoop3
4.0.3
+
+ 2.9.0
+
+ 2.35
+
+ 2.6.1
+
+ 2.1.6
+
+ 1.3.5
+
+ 1.0
+
+ 1.0.3
+ ranger
+
+ auth
+
+