columns, DrillAccessType operator) {
+ if (!enabled || authorizer == null || isSystemSchema(schema)) {
+ return true; // fail-open when disabled
+ }
+
+ try {
+ DrillResource resource = new DrillResource();
+ resource.setUser(user);
+ resource.setGroups(getUserGroups(user));
+ resource.setDataSource(dataSource != null ? dataSource : "drill");
+ resource.setSchema(schema);
+ resource.setTable(table);
+ resource.setColumns(columns);
+ return authorizer.checkColumnAccess(resource, operator);
+ } catch (Exception e) {
+ LOG.error("Error checking column access for user={}, schema={}, table={}", user, schema, table, e);
+ return false; // fail-closed on error
+ }
+ }
+
+ /**
+ * 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()) {
+ return true;
+ }
+ // 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());
+ }
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizer.java b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizer.java
new file mode 100644
index 00000000000..f3c5715c17f
--- /dev/null
+++ b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizer.java
@@ -0,0 +1,185 @@
+/*
+ * 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.authorization.drill.resource.DrillAccessResource;
+import org.apache.ranger.authorization.drill.resource.DrillAccessType;
+import org.apache.ranger.authorization.drill.resource.DrillRangerAccessRequest;
+import org.apache.ranger.authorization.drill.resource.DrillResource;
+import org.apache.ranger.plugin.policyengine.RangerAccessRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+public class DrillAuthorizer {
+ private static final Logger LOG = LoggerFactory.getLogger(DrillAuthorizer.class);
+ private RangerBaseAuthorizer authorizer;
+
+ public DrillAuthorizer(String serviceName) {
+ authorizer = RangerBaseAuthorizer.getInstance();
+ authorizer.init(serviceName);
+ }
+
+ private boolean checkPermission(DrillRangerAccessRequest request) {
+ return authorizer.isAccessAllowed(request.toRangerRequest());
+ }
+
+ /**
+ * Build a DrillRangerAccessRequest from the given resource and access type, then check
+ * permission. This abstracts the common logic shared by table-level and column-level
+ * access checks.
+ *
+ * @param resource the DrillResource providing user, groups, etc.
+ * @param drillAccessResource the DrillAccessResource describing the accessed entity
+ * @param operator the access type to check
+ * @return the permission check result
+ */
+ private boolean checkAccess(DrillResource resource, DrillAccessResource drillAccessResource,
+ DrillAccessType operator, RangerAccessRequest.ResourceMatchingScope scope) {
+ Set groups = new HashSet<>();
+ if (resource.getGroups() != null) {
+ groups.addAll(resource.getGroups());
+ }
+
+ DrillRangerAccessRequest request = DrillRangerAccessRequest.builder()
+ .user(resource.getUser())
+ .groups(groups)
+ .resource(drillAccessResource)
+ .accessType(operator)
+ .resourceMatchingScope(scope)
+ .build();
+
+ return checkPermission(request);
+ }
+
+ public boolean checkTableAccess(DrillResource resource, DrillAccessType operator) {
+ if (!validateResource(resource, ValidationLevel.TABLE)) {
+ LOG.warn("MetaStoreResource validation failed for table access check");
+ return false;
+ }
+ Optional schema = Optional.ofNullable(resource.getSchema());
+ Optional table = Optional.ofNullable(resource.getTable());
+ DrillAccessResource drillAccessResource = new DrillAccessResource(resource.getDataSource(),
+ schema, 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(resource, drillAccessResource, operator,
+ RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS);
+
+ LOG.info("checkTableAccess result for user={}, datasource={}, schema={}, table={}, " +
+ "operator={}: result={}",
+ resource.getUser(), resource.getDataSource(), resource.getSchema(),
+ resource.getTable(), operator.name(), result);
+
+ return result;
+ }
+
+ /**
+ * Validate that required fields of MetaStoreResource are non-empty (full validation, defaults
+ * to column level)
+ *
+ * @param resource the resource object to validate
+ * @return true if validation passes, false otherwise
+ */
+ public boolean validateResource(DrillResource resource) {
+ return validateResource(resource, ValidationLevel.COLUMN);
+ }
+
+ public boolean checkColumnAccess(DrillResource resource, DrillAccessType operator) {
+ if (!validateResource(resource, ValidationLevel.COLUMN)) {
+ LOG.warn("MetaStoreResource validation failed for table access check");
+ return false;
+ }
+ Optional schema = Optional.ofNullable(resource.getSchema());
+ Optional table = Optional.ofNullable(resource.getTable());
+
+ for (String column : resource.getColumns()) {
+ Optional columnOpt = Optional.of(column);
+ DrillAccessResource drillAccessResource = new DrillAccessResource(resource.getDataSource(), schema, table, columnOpt);
+
+ // Column-level check uses SELF for exact column matching: only policies
+ // whose column resource matches the requested column will be applied.
+ boolean allowed = checkAccess(resource, drillAccessResource, operator,
+ RangerAccessRequest.ResourceMatchingScope.SELF);
+
+ LOG.info("checkColumnAccess result for user={}, datasource={}, schema={}, table={}, " +
+ "column={}, operator={}: result={}",
+ resource.getUser(), resource.getDataSource(), resource.getSchema(),
+ resource.getTable(), column, operator.name(), allowed);
+
+ if (!allowed) {
+ // Fail fast on first denied column — no need to check the rest.
+ LOG.warn("Column access denied for user={}, column={}.{}.{}",
+ resource.getUser(), resource.getDataSource(), resource.getSchema(),
+ resource.getTable());
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Validate that required fields of MetaStoreResource are non-empty (with specified validation
+ * level)
+ *
+ * @param resource the resource object to validate
+ * @param validationLevel the validation level
+ * @return true if validation passes, false otherwise
+ */
+ private boolean validateResource(DrillResource resource, ValidationLevel validationLevel) {
+ boolean allValid = true;
+ if (resource == null) {
+ LOG.error("MetaStoreResource is null");
+ return false;
+ }
+ if (resource.getUser() == null || resource.getUser().trim().isEmpty()) {
+ LOG.error("MetaStoreResource user is null or empty");
+ return false;
+ }
+ if (resource.getDataSource() == null || resource.getDataSource().trim().isEmpty()) {
+ LOG.error("MetaStoreResource dataSource is null or empty");
+ return false;
+ }
+ if (validationLevel.ordinal() >= ValidationLevel.SCHEMA.ordinal()) {
+ if (resource.getSchema() == null || resource.getSchema().trim().isEmpty()) {
+ LOG.error("MetaStoreResource schema is null or empty");
+ return false;
+ }
+ }
+ if (validationLevel.ordinal() >= ValidationLevel.TABLE.ordinal()) {
+ if (resource.getTable() == null || resource.getTable().trim().isEmpty()) {
+ LOG.error("MetaStoreResource table is null or empty");
+ return false;
+ }
+ }
+ if (validationLevel.ordinal() >= ValidationLevel.COLUMN.ordinal()) {
+ if (resource.getColumns() == null || resource.getColumns().isEmpty()) {
+ LOG.error("MetaStoreResource columns is null or empty");
+ return false;
+ }
+ allValid = resource.getColumns().stream()
+ .allMatch(column -> column != null && !column.trim().isEmpty());
+ }
+ return allValid;
+ }
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizer.java b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizer.java
new file mode 100644
index 00000000000..65d4a87bb0a
--- /dev/null
+++ b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerBaseAuthorizer.java
@@ -0,0 +1,98 @@
+/*
+ * 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 LOG = LoggerFactory.getLogger(RangerBaseAuthorizer.class);
+
+ private static RangerBaseAuthorizer instance;
+ 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();
+ LOG.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) {
+ LOG.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();
+ }
+ }
+
+ public void cleanUp() {
+ if (plugin != null) {
+ plugin.cleanup();
+ }
+ }
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerDrillPlugin.java b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/RangerDrillPlugin.java
new file mode 100644
index 00000000000..18ad05fd3f9
--- /dev/null
+++ b/drill-ranger/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_PRESTO_APPID = "drill";
+
+ public RangerDrillPlugin(String serviceName) {
+ super(SERVICE_TYPE, serviceName, RANGER_PRESTO_APPID);
+ }
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/ValidationLevel.java b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/ValidationLevel.java
new file mode 100644
index 00000000000..f3eefedb012
--- /dev/null
+++ b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/authorizer/ValidationLevel.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;
+
+/**
+ * Resource validation level enum.
+ * Controls the depth of validation in the validateResource method.
+ */
+public enum ValidationLevel {
+ /** Validate up to the datasource level (user, dataSource). */
+ DATASOURCE,
+ /** Validate up to the schema level (user, dataSource, schema). */
+ SCHEMA,
+ /** Validate up to the table level (user, dataSource, schema, table). */
+ TABLE,
+ /** Full validation including columns (user, dataSource, schema, table, columns). */
+ COLUMN
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessResource.java b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessResource.java
new file mode 100644
index 00000000000..950e8dff5ab
--- /dev/null
+++ b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessResource.java
@@ -0,0 +1,94 @@
+/*
+ * 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 LOG = LoggerFactory.getLogger(DrillAccessResource.class);
+
+ public DrillAccessResource() {
+ }
+
+ 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 (LOG.isDebugEnabled()) {
+ LOG.debug("AccessResource set value: {} = {}", key, value);
+ }
+ }
+ }
+
+ public DrillAccessResource(String catalogName, Optional schema, Optional table) {
+ setValue(RangerDrillResource.DATASOURCE.toString(), catalogName);
+ schema.ifPresent(s -> setValue(RangerDrillResource.SCHEMA.toString(), s));
+ table.ifPresent(s -> setValue(RangerDrillResource.TABLE.toString(), s));
+ }
+
+ public DrillAccessResource(String catalogName, Optional schema, Optional table,
+ Optional column) {
+ setValue(RangerDrillResource.DATASOURCE.toString(), catalogName);
+ 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 getCatalogName() {
+ return (String) getValue(RangerDrillResource.DATASOURCE.toString());
+ }
+
+ public String getTable() {
+ return (String) getValue(RangerDrillResource.TABLE.toString());
+ }
+
+ public String getCatalog() {
+ return (String) getValue(RangerDrillResource.SCHEMA.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/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessType.java b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillAccessType.java
new file mode 100644
index 00000000000..0d1a0c2301b
--- /dev/null
+++ b/drill-ranger/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, SHOW, USE, SELECT, INSERT, DELETE;
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequest.java b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequest.java
new file mode 100644
index 00000000000..37aad503693
--- /dev/null
+++ b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequest.java
@@ -0,0 +1,149 @@
+/*
+ * 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+public class DrillRangerAccessRequest {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DrillRangerAccessRequest.class);
+
+ 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/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillResource.java b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillResource.java
new file mode 100644
index 00000000000..cc8d972be16
--- /dev/null
+++ b/drill-ranger/drill-ranger-plugin/src/main/java/org/apache/ranger/authorization/drill/resource/DrillResource.java
@@ -0,0 +1,76 @@
+/*
+ * 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 java.util.Set;
+
+public class DrillResource {
+ private String user;
+ private Set groups;
+ private String dataSource;
+ private String schema;
+ private String table;
+ private Set columns;
+
+ public String getUser() {
+ return user;
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+
+ public Set getGroups() {
+ return groups;
+ }
+
+ public void setGroups(Set groups) {
+ this.groups = groups;
+ }
+
+ public String getDataSource() {
+ return dataSource;
+ }
+
+ public void setDataSource(String dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public String getSchema() {
+ return schema;
+ }
+
+ public void setSchema(String schema) {
+ this.schema = schema;
+ }
+
+ public String getTable() {
+ return table;
+ }
+
+ public void setTable(String table) {
+ this.table = table;
+ }
+
+ public Set getColumns() {
+ return columns;
+ }
+
+ public void setColumns(Set columns) {
+ this.columns = columns;
+ }
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControlTest.java b/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControlTest.java
new file mode 100644
index 00000000000..9aa3f02fdf4
--- /dev/null
+++ b/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAccessControlTest.java
@@ -0,0 +1,261 @@
+/*
+ * 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.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.MockedStatic;
+
+import java.lang.reflect.Field;
+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.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link DrillAccessControl} static facade.
+ * Covers fail-open (disabled), system-schema bypass, fail-closed (exception)
+ * and user-group resolution behavior.
+ */
+public class DrillAccessControlTest {
+
+ /**
+ * 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 {@code checkTableAccess}/{@code checkColumnAccess} — gets a deterministic
+ * empty group set without touching the OS.
+ */
+ private MockedStatic ugiMock;
+ private UserGroupInformation mockUgi;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ setStaticField(DrillAccessControl.class, "enabled", false);
+ setStaticField(DrillAccessControl.class, "authorizer", null);
+
+ // 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(
+ org.mockito.ArgumentMatchers.anyString())).thenReturn(mockUgi);
+ when(mockUgi.getGroupNames()).thenReturn(new String[0]);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ if (ugiMock != null) {
+ ugiMock.close();
+ ugiMock = null;
+ }
+ setStaticField(DrillAccessControl.class, "enabled", false);
+ setStaticField(DrillAccessControl.class, "authorizer", null);
+ }
+
+ @Test
+ public void isEnabled_returnsFalse_beforeInit() {
+ assertFalse(DrillAccessControl.isEnabled());
+ }
+
+ @Test
+ public void checkTableAccess_returnsTrue_whenNotEnabled() {
+ assertTrue(DrillAccessControl.checkTableAccess(
+ "root", "mysql", "shf", "orders", DrillAccessType.SELECT));
+ }
+
+ @Test
+ public void checkColumnAccess_returnsTrue_whenNotEnabled() {
+ Set columns = new HashSet<>();
+ columns.add("amount");
+ assertTrue(DrillAccessControl.checkColumnAccess(
+ "root", "mysql", "shf", "orders", columns, DrillAccessType.SELECT));
+ }
+
+ @Test
+ public void checkTableAccess_bypassesSystemSchema_informationSchema() throws Exception {
+ DrillAuthorizer mockAuthorizer = mock(DrillAuthorizer.class);
+ setStaticField(DrillAccessControl.class, "enabled", true);
+ setStaticField(DrillAccessControl.class, "authorizer", mockAuthorizer);
+
+ assertTrue(DrillAccessControl.checkTableAccess(
+ "root", "dfs", "INFORMATION_SCHEMA", "TABLES", DrillAccessType.SELECT));
+ verify(mockAuthorizer, never()).checkTableAccess(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any());
+ }
+
+ @Test
+ public void checkTableAccess_bypassesSystemSchema_sys_caseInsensitive() throws Exception {
+ DrillAuthorizer mockAuthorizer = mock(DrillAuthorizer.class);
+ setStaticField(DrillAccessControl.class, "enabled", true);
+ setStaticField(DrillAccessControl.class, "authorizer", mockAuthorizer);
+
+ for (String schema : new String[] {"sys", "Sys", "SYS"}) {
+ assertTrue(DrillAccessControl.checkTableAccess(
+ "root", "dfs", schema, "DRILLBITS", DrillAccessType.SELECT),
+ "schema=" + schema + " should bypass authorization");
+ }
+ verify(mockAuthorizer, never()).checkTableAccess(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any());
+ }
+
+ @Test
+ public void checkTableAccess_bypassesSystemSchema_compoundPath() throws Exception {
+ DrillAuthorizer mockAuthorizer = mock(DrillAuthorizer.class);
+ setStaticField(DrillAccessControl.class, "enabled", true);
+ setStaticField(DrillAccessControl.class, "authorizer", mockAuthorizer);
+
+ // Top-level segment "information_schema" should match, even with compound path
+ assertTrue(DrillAccessControl.checkTableAccess(
+ "root", "dfs", "information_schema.tables", "COLUMNS", DrillAccessType.SELECT));
+ verify(mockAuthorizer, never()).checkTableAccess(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any());
+ }
+
+ @Test
+ public void checkTableAccess_bypassesSystemSchema_nullSchema() throws Exception {
+ DrillAuthorizer mockAuthorizer = mock(DrillAuthorizer.class);
+ setStaticField(DrillAccessControl.class, "enabled", true);
+ setStaticField(DrillAccessControl.class, "authorizer", mockAuthorizer);
+
+ assertTrue(DrillAccessControl.checkTableAccess(
+ "root", "dfs", null, "orders", DrillAccessType.SELECT));
+ }
+
+ @Test
+ public void checkTableAccess_bypassesSystemSchema_emptySchema() throws Exception {
+ DrillAuthorizer mockAuthorizer = mock(DrillAuthorizer.class);
+ setStaticField(DrillAccessControl.class, "enabled", true);
+ setStaticField(DrillAccessControl.class, "authorizer", mockAuthorizer);
+
+ assertTrue(DrillAccessControl.checkTableAccess(
+ "root", "dfs", "", "orders", DrillAccessType.SELECT));
+ assertTrue(DrillAccessControl.checkTableAccess(
+ "root", "dfs", " ", "orders", DrillAccessType.SELECT));
+ }
+
+ @Test
+ public void checkTableAccess_delegatesToAuthorizer_whenEnabled() throws Exception {
+ DrillAuthorizer mockAuthorizer = mock(DrillAuthorizer.class);
+ when(mockAuthorizer.checkTableAccess(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.eq(DrillAccessType.SELECT))).thenReturn(true);
+ setStaticField(DrillAccessControl.class, "enabled", true);
+ setStaticField(DrillAccessControl.class, "authorizer", mockAuthorizer);
+
+ boolean result = DrillAccessControl.checkTableAccess(
+ "root", "mysql", "shf", "orders", DrillAccessType.SELECT);
+ assertTrue(result);
+ verify(mockAuthorizer).checkTableAccess(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.eq(DrillAccessType.SELECT));
+ }
+
+ @Test
+ public void checkTableAccess_returnsFalse_whenAuthorizerThrows() throws Exception {
+ DrillAuthorizer mockAuthorizer = mock(DrillAuthorizer.class);
+ when(mockAuthorizer.checkTableAccess(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any())).thenThrow(new RuntimeException("boom"));
+ setStaticField(DrillAccessControl.class, "enabled", true);
+ setStaticField(DrillAccessControl.class, "authorizer", mockAuthorizer);
+
+ boolean result = DrillAccessControl.checkTableAccess(
+ "root", "mysql", "shf", "orders", DrillAccessType.SELECT);
+ assertFalse(result);
+ }
+
+ @Test
+ public void checkColumnAccess_returnsFalse_whenAuthorizerThrows() throws Exception {
+ DrillAuthorizer mockAuthorizer = mock(DrillAuthorizer.class);
+ when(mockAuthorizer.checkColumnAccess(
+ org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any())).thenThrow(new RuntimeException("boom"));
+ setStaticField(DrillAccessControl.class, "enabled", true);
+ setStaticField(DrillAccessControl.class, "authorizer", mockAuthorizer);
+
+ Set columns = new HashSet<>();
+ columns.add("amount");
+ boolean result = DrillAccessControl.checkColumnAccess(
+ "root", "mysql", "shf", "orders", columns, DrillAccessType.SELECT);
+ assertFalse(result);
+ }
+
+ @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("root");
+ 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("root");
+ assertNotNull(groups);
+ assertTrue(groups.isEmpty());
+ }
+
+ // ========================================================================
+ // Reflection helpers
+ // ========================================================================
+
+ private static void setStaticField(Class> clazz, String fieldName, Object value) throws Exception {
+ Field field = clazz.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(null, value);
+ }
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizerTest.java b/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizerTest.java
new file mode 100644
index 00000000000..9f4878245df
--- /dev/null
+++ b/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/authorizer/DrillAuthorizerTest.java
@@ -0,0 +1,229 @@
+/*
+ * 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.authorization.drill.resource.DrillAccessType;
+import org.apache.ranger.authorization.drill.resource.DrillResource;
+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.HashSet;
+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.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.mockStatic;
+
+/**
+ * Unit tests for {@link DrillAuthorizer}.
+ * Covers resource validation, table-level and column-level access checks,
+ * and per-column fail-fast behavior.
+ */
+public class DrillAuthorizerTest {
+
+ /**
+ * 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 DrillResource buildValidResource() {
+ DrillResource r = new DrillResource();
+ r.setUser("root");
+ r.setDataSource("mysql");
+ r.setSchema("shf");
+ r.setTable("orders");
+ Set columns = new LinkedHashSet<>();
+ columns.add("amount");
+ r.setColumns(columns);
+ return r;
+ }
+
+ @Test
+ public void validateResource_nullResource_returnsFalse() {
+ DrillAuthorizer authorizer = newAuthorizer(mock(RangerBaseAuthorizer.class));
+ assertFalse(authorizer.validateResource(null));
+ }
+
+ @Test
+ public void validateResource_emptyUser_returnsFalse() {
+ DrillAuthorizer authorizer = newAuthorizer(mock(RangerBaseAuthorizer.class));
+ DrillResource r = buildValidResource();
+ r.setUser(null);
+ assertFalse(authorizer.validateResource(r));
+
+ r.setUser("");
+ assertFalse(authorizer.validateResource(r));
+ }
+
+ @Test
+ public void validateResource_emptyDataSource_returnsFalse() {
+ DrillAuthorizer authorizer = newAuthorizer(mock(RangerBaseAuthorizer.class));
+ DrillResource r = buildValidResource();
+ r.setDataSource(null);
+ assertFalse(authorizer.validateResource(r));
+
+ r.setDataSource("");
+ assertFalse(authorizer.validateResource(r));
+ }
+
+ @Test
+ public void validateResource_emptySchema_returnsFalse_atSchemaLevel() {
+ DrillAuthorizer authorizer = newAuthorizer(mock(RangerBaseAuthorizer.class));
+ DrillResource r = buildValidResource();
+ r.setSchema(null);
+ // COLUMN-level validate requires schema non-empty too
+ assertFalse(authorizer.validateResource(r));
+
+ r.setSchema("");
+ assertFalse(authorizer.validateResource(r));
+ }
+
+ @Test
+ public void validateResource_emptyTable_returnsFalse_atTableLevel() {
+ DrillAuthorizer authorizer = newAuthorizer(mock(RangerBaseAuthorizer.class));
+ DrillResource r = buildValidResource();
+ r.setTable(null);
+ assertFalse(authorizer.validateResource(r));
+
+ r.setTable("");
+ assertFalse(authorizer.validateResource(r));
+ }
+
+ @Test
+ public void validateResource_emptyColumns_returnsFalse_atColumnLevel() {
+ DrillAuthorizer authorizer = newAuthorizer(mock(RangerBaseAuthorizer.class));
+ DrillResource r = buildValidResource();
+ r.setColumns(null);
+ assertFalse(authorizer.validateResource(r));
+
+ r.setColumns(Collections.emptySet());
+ assertFalse(authorizer.validateResource(r));
+ }
+
+ @Test
+ public void validateResource_validResource_returnsTrue() {
+ DrillAuthorizer authorizer = newAuthorizer(mock(RangerBaseAuthorizer.class));
+ DrillResource r = buildValidResource();
+ assertTrue(authorizer.validateResource(r));
+ }
+
+ @Test
+ public void checkTableAccess_returnsFalse_whenValidationFails() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ DrillResource r = buildValidResource();
+ r.setTable(null); // missing table -> validation fails at TABLE level
+ assertFalse(authorizer.checkTableAccess(r, DrillAccessType.SELECT));
+ verify(mockBase, never()).isAccessAllowed(any());
+ }
+
+ @Test
+ public void checkTableAccess_delegatesToAuthorizer_withSelfOrDescendantsScope() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ when(mockBase.isAccessAllowed(any())).thenReturn(true);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ DrillResource r = buildValidResource();
+ boolean result = authorizer.checkTableAccess(r, DrillAccessType.SELECT);
+
+ assertTrue(result);
+ ArgumentCaptor captor = ArgumentCaptor.forClass(RangerAccessRequest.class);
+ verify(mockBase).isAccessAllowed(captor.capture());
+ RangerAccessRequest captured = captor.getValue();
+ assertEquals(
+ RangerAccessRequest.ResourceMatchingScope.SELF_OR_DESCENDANTS,
+ captured.getResourceMatchingScope());
+ }
+
+ @Test
+ public void checkColumnAccess_returnsFalse_whenValidationFails() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ DrillResource r = buildValidResource();
+ r.setColumns(Collections.emptySet()); // validation fails
+ assertFalse(authorizer.checkColumnAccess(r, DrillAccessType.SELECT));
+ verify(mockBase, never()).isAccessAllowed(any());
+ }
+
+ @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);
+
+ DrillResource r = buildValidResource();
+ // Use LinkedHashSet for deterministic iteration order
+ Set columns = new LinkedHashSet<>(Arrays.asList("c1", "c2", "c3"));
+ r.setColumns(columns);
+
+ boolean result = authorizer.checkColumnAccess(r, DrillAccessType.SELECT);
+ assertFalse(result);
+ // 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);
+
+ DrillResource r = buildValidResource();
+ Set columns = new LinkedHashSet<>(Arrays.asList("c1", "c2", "c3"));
+ r.setColumns(columns);
+
+ boolean result = authorizer.checkColumnAccess(r, DrillAccessType.SELECT);
+ assertTrue(result);
+ verify(mockBase, times(3)).isAccessAllowed(any());
+ }
+
+ @Test
+ public void checkColumnAccess_emptyColumnInSet_returnsFalse() {
+ RangerBaseAuthorizer mockBase = mock(RangerBaseAuthorizer.class);
+ DrillAuthorizer authorizer = newAuthorizer(mockBase);
+
+ DrillResource r = buildValidResource();
+ Set columns = new HashSet<>();
+ columns.add("");
+ r.setColumns(columns);
+
+ assertFalse(authorizer.checkColumnAccess(r, DrillAccessType.SELECT));
+ verify(mockBase, never()).isAccessAllowed(any());
+ }
+}
diff --git a/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillAccessResourceTest.java b/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillAccessResourceTest.java
new file mode 100644
index 00000000000..1cf70741710
--- /dev/null
+++ b/drill-ranger/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.getCatalogName());
+ 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.getCatalogName());
+ 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.getCatalogName());
+ assertNotNull(r.getTable());
+ }
+
+ @Test
+ public void constructor_emptyTable_doesNotSetTableKey() {
+ DrillAccessResource r = new DrillAccessResource(
+ "mysql", Optional.of("shf"), Optional.empty());
+ assertEquals("mysql", r.getCatalogName());
+ 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.getCatalogName());
+ }
+
+ @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/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequestTest.java b/drill-ranger/drill-ranger-plugin/src/test/java/org/apache/ranger/authorization/drill/resource/DrillRangerAccessRequestTest.java
new file mode 100644
index 00000000000..5ee53800118
--- /dev/null
+++ b/drill-ranger/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/drill-ranger/drill-ranger-service/pom.xml b/drill-ranger/drill-ranger-service/pom.xml
new file mode 100644
index 00000000000..899bcf5726f
--- /dev/null
+++ b/drill-ranger/drill-ranger-service/pom.xml
@@ -0,0 +1,139 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.drill
+ drill-ranger-parent
+ 1.23.0-SNAPSHOT
+
+
+ ranger-drill-service
+ Drill : Ranger Drill Service Plugin
+
+ Apache Ranger service plugin for Apache Drill.
+ Deployed to Ranger Admin (NOT to the Drillbit). Provides validateConfig
+ (connection test) and lookupResource (schema/table/column autocomplete in
+ the Ranger policy editor) by connecting to Drill via REST API.
+ Compiled with JDK 8 bytecode for compatibility with Ranger Admin running
+ on JDK 8. Uses only JDK-standard HTTP (HttpURLConnection) and Jackson
+ (provided by Ranger Admin) with no Drill dependencies.
+
+
+
+
+
+
+
+
+
+ org.apache.ranger
+ ranger-plugins-common
+ ${ranger.version}
+ provided
+
+
+
+ commons-logging
+ commons-logging
+
+
+ javax.servlet
+ javax.servlet-api
+
+
+ javax.servlet.jsp
+ jsp-api
+
+
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-api
+ provided
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 8
+
+
+
+
+
diff --git a/drill-ranger/drill-ranger-service/src/main/java/org/apache/ranger/services/drill/RangerServiceDrill.java b/drill-ranger/drill-ranger-service/src/main/java/org/apache/ranger/services/drill/RangerServiceDrill.java
new file mode 100644
index 00000000000..3bce42d42ae
--- /dev/null
+++ b/drill-ranger/drill-ranger-service/src/main/java/org/apache/ranger/services/drill/RangerServiceDrill.java
@@ -0,0 +1,457 @@
+/*
+ * 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.services.drill;
+
+import org.apache.ranger.plugin.model.RangerPolicy;
+import org.apache.ranger.plugin.model.RangerService;
+import org.apache.ranger.plugin.model.RangerServiceDef;
+import org.apache.ranger.plugin.service.RangerBaseService;
+import org.apache.ranger.plugin.service.ResourceLookupContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Ranger service plugin for Apache Drill.
+ *
+ * Deployed into Ranger Admin (NOT into the Drillbit). Provides:
+ *
+ * - {@link #validateConfig()} - tests connectivity to the Drill cluster
+ * by calling the Drill REST API ({@code POST /query.json}).
+ * - {@link #lookupResource(ResourceLookupContext)} - enumerates
+ * datasource / schema / table / column resources via
+ * {@code INFORMATION_SCHEMA} queries through the REST API, so the
+ * Ranger policy editor can auto-complete resource paths.
+ *
+ *
+ * Uses Drill's REST API (default port 8047) instead of JDBC, so this
+ * module can be compiled with JDK 8 and deployed into a JDK 8 Ranger Admin
+ * without pulling in Drill's JDK 11+ dependencies.
+ *
+ *
Connection configuration (must match the service-def JSON
+ * {@code serviceConfigOptions}):
+ *
+ * - {@code username} - Drill user name (required)
+ * - {@code password} - Drill user password (optional)
+ * - {@code drill.connection.url} - Drill REST endpoint, e.g.
+ * {@code http://host:8047} (required).
+ *
+ */
+public class RangerServiceDrill extends RangerBaseService {
+
+ private static final Logger LOG = LoggerFactory.getLogger(RangerServiceDrill.class);
+
+ // Service config keys (must match ranger-servicedef-drill.json)
+ private static final String CONFIG_USERNAME = "username";
+ private static final String CONFIG_PASSWORD = "password";
+ private static final String CONFIG_DRILL_URL = "drill.connection.url";
+
+ // Resource names (must match ranger-servicedef-drill.json, lowercase per Ranger naming rules)
+ private static final String RESOURCE_DATASOURCE = "datasource";
+ private static final String RESOURCE_SCHEMA = "schema";
+ private static final String RESOURCE_TABLE = "table";
+ private static final String RESOURCE_COLUMN = "column";
+
+ // HTTP connect / read timeout (milliseconds)
+ private static final int CONNECT_TIMEOUT_MS = 10_000;
+ private static final int READ_TIMEOUT_MS = 30_000;
+
+ // SQL templates for resource lookup. Each %s is filled via String.format
+ // with the corresponding escaped resource value. TABLES and COLUMNS are
+ // reserved keywords in Drill SQL and must be backtick-quoted.
+ private static final String SQL_VALIDATE_CONNECTION = "SELECT 1";
+
+ private static final String SQL_LOOKUP_DATASOURCE =
+ "SELECT DISTINCT SPLIT_PART(SCHEMA_NAME, '.', 1) AS DATASOURCE "
+ + "FROM INFORMATION_SCHEMA.SCHEMATA "
+ + "WHERE SCHEMA_NAME LIKE '%.%' "
+ + "ORDER BY 1";
+
+ // %s = datasource (e.g. "mysql")
+ private static final String SQL_LOOKUP_SCHEMA =
+ "SELECT SPLIT_PART(SCHEMA_NAME, '.', 2) AS SCHEMA "
+ + "FROM INFORMATION_SCHEMA.SCHEMATA "
+ + "WHERE SCHEMA_NAME LIKE '%s.%%' "
+ + "ORDER BY 1";
+
+ // %s = full table schema (e.g. "mysql.shf")
+ private static final String SQL_LOOKUP_TABLE =
+ "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.`TABLES` "
+ + "WHERE TABLE_SCHEMA = '%s' "
+ + "ORDER BY 1";
+
+ // %1$s = full table schema, %2$s = table name
+ private static final String SQL_LOOKUP_COLUMN =
+ "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.`COLUMNS` "
+ + "WHERE TABLE_SCHEMA = '%s' "
+ + "AND TABLE_NAME = '%s' "
+ + "ORDER BY 1";
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ @Override
+ public void init(RangerServiceDef serviceDef, RangerService service) {
+ super.init(serviceDef, service);
+ LOG.debug("RangerServiceDrill initialized for service={}",
+ service != null ? service.getName() : "null");
+ }
+
+ /**
+ * Validates the service configuration by testing connectivity to Drill.
+ *
+ * @return a map with {@code status} = {@code SUCCESS} or {@code FAILURE}
+ * and a human-readable {@code message}.
+ */
+ @Override
+ public Map validateConfig() throws Exception {
+ Map result = new HashMap<>();
+
+ String username = getConfig(CONFIG_USERNAME);
+ if (isBlank(username)) {
+ return failure(result, "Drill user name is required");
+ }
+ String baseUrl = getConfig(CONFIG_DRILL_URL);
+ if (isBlank(baseUrl)) {
+ return failure(result, "Drill connection URL is required");
+ }
+ String password = getConfig(CONFIG_PASSWORD);
+
+ String normalizedUrl;
+ try {
+ normalizedUrl = buildBaseUrl(baseUrl);
+ } catch (IllegalArgumentException e) {
+ return failure(result, "Invalid drill.connection.url: " + e.getMessage());
+ }
+
+ LOG.info("Validating Drill service connection to {}", normalizedUrl);
+ try {
+ String response = executeQuery(normalizedUrl, username, password, SQL_VALIDATE_CONNECTION);
+ // A successful query returns JSON with a "rows" array
+ JsonNode root = MAPPER.readTree(response);
+ if (root != null && root.has("rows") && root.get("rows").isArray()) {
+ result.put("status", "SUCCESS");
+ result.put("message", "Connection test succeeded");
+ LOG.info("Drill connection validation succeeded for {}", normalizedUrl);
+ } else {
+ return failure(result, "Unexpected response from Drill: " + response);
+ }
+ } catch (Exception e) {
+ LOG.error("Drill connection validation failed for url={}", normalizedUrl, e);
+ return failure(result, "Connection test failed: " + e.getMessage());
+ }
+ return result;
+ }
+
+ /**
+ * Lists Drill resources for the Ranger policy editor autocomplete.
+ *
+ * Supported resource levels (must match the service-def JSON):
+ *
+ * - {@code datasource} - distinct storage plugins from
+ * {@code INFORMATION_SCHEMA.SCHEMATA}
+ * - {@code schema} - schema names filtered by the selected datasource
+ * - {@code table} - table names filtered by datasource + schema
+ * - {@code column} - column names filtered by datasource + schema + table
+ *
+ *
+ * @param context carries the requested resource name and the already-selected
+ * parent resources in {@link ResourceLookupContext#getResources()}
+ * @return a list of matching resource names (never {@code null})
+ */
+ @Override
+ public List lookupResource(ResourceLookupContext context) throws Exception {
+ if (context == null) {
+ return Collections.emptyList();
+ }
+ String resourceName = context.getResourceName();
+ // getResources() returns Map> in Ranger 2.8.0:
+ // each parent resource name maps to a list of selected values.
+ Map> hints = context.getResources() != null
+ ? context.getResources() : Collections.emptyMap();
+
+ if (isBlank(resourceName)) {
+ return Collections.emptyList();
+ }
+
+ String username = getConfig(CONFIG_USERNAME);
+ String password = getConfig(CONFIG_PASSWORD);
+ String baseUrl = buildBaseUrl(getConfig(CONFIG_DRILL_URL));
+
+ LOG.debug("lookupResource: resource={}, hints={}", resourceName, hints);
+
+ try {
+ switch (resourceName) {
+ case RESOURCE_DATASOURCE:
+ // Drill's INFORMATION_SCHEMA.SCHEMATA has no STORAGE_PLUGIN column.
+ // The datasource (storage plugin name) is the first segment of
+ // SCHEMA_NAME (e.g. "mysql.shf" -> "mysql"). Use SUBSTR_INDEX to
+ // extract it, then DISTINCT to deduplicate.
+ return extractFirstColumnValues(executeQuery(baseUrl, username, password,
+ SQL_LOOKUP_DATASOURCE));
+ case RESOURCE_SCHEMA: {
+ String datasource = firstHint(hints, RESOURCE_DATASOURCE);
+ if (isBlank(datasource)) {
+ return Collections.emptyList();
+ }
+ // For a given datasource, list schema names by stripping the
+ // "datasource." prefix from SCHEMA_NAME (e.g. "mysql.shf" -> "shf").
+ // Schemas without a dot (e.g. plain "mysql") are filtered out.
+ String sql = String.format(SQL_LOOKUP_SCHEMA, escapeSql(datasource));
+ return extractFirstColumnValues(executeQuery(baseUrl, username, password, sql));
+ }
+ case RESOURCE_TABLE: {
+ String datasource = firstHint(hints, RESOURCE_DATASOURCE);
+ String schema = firstHint(hints, RESOURCE_SCHEMA);
+ if (isBlank(datasource) || isBlank(schema)) {
+ return Collections.emptyList();
+ }
+ // TABLE_SCHEMA in INFORMATION_SCHEMA.`TABLES` uses the full qualified
+ // form "datasource.schema" (e.g. "mysql.shf"), so concatenate the
+ // selected datasource and schema before filtering.
+ // NOTE: TABLES is a reserved keyword in Drill SQL and must be
+ // backtick-quoted; without quotes the parser rejects the query.
+ String tableSchema = escapeSql(datasource) + "." + escapeSql(schema);
+ String sql = String.format(SQL_LOOKUP_TABLE, tableSchema);
+ return extractFirstColumnValues(executeQuery(baseUrl, username, password, sql));
+ }
+ case RESOURCE_COLUMN: {
+ String datasource = firstHint(hints, RESOURCE_DATASOURCE);
+ String schema = firstHint(hints, RESOURCE_SCHEMA);
+ String table = firstHint(hints, RESOURCE_TABLE);
+ if (isBlank(datasource) || isBlank(schema) || isBlank(table)) {
+ return Collections.emptyList();
+ }
+ // COLUMNS is also a reserved keyword in Drill SQL — backtick-quote it.
+ String tableSchema = escapeSql(datasource) + "." + escapeSql(schema);
+ String sql = String.format(SQL_LOOKUP_COLUMN, tableSchema, escapeSql(table));
+ return extractFirstColumnValues(executeQuery(baseUrl, username, password, sql));
+ }
+ default:
+ LOG.warn("Unknown resource name: {}", resourceName);
+ return Collections.emptyList();
+ }
+ } catch (Exception e) {
+ LOG.error("lookupResource failed for resource={}, hints={}", resourceName, hints, e);
+ throw e;
+ }
+ }
+
+ @Override
+ public List getDefaultRangerPolicies() throws Exception {
+ return super.getDefaultRangerPolicies();
+ }
+
+ // ========================================================================
+ // REST API helpers
+ // ========================================================================
+
+ /**
+ * Normalizes the user-supplied {@code drill.connection.url} to a base URL
+ * like {@code http://host:8047}. Trims trailing slashes.
+ */
+ static String buildBaseUrl(String configuredUrl) {
+ if (isBlank(configuredUrl)) {
+ throw new IllegalArgumentException("drill.connection.url is empty");
+ }
+ String url = configuredUrl.trim();
+ // Strip trailing slashes
+ while (url.endsWith("/")) {
+ url = url.substring(0, url.length() - 1);
+ }
+ return url;
+ }
+
+ /**
+ * Executes a SQL query via the Drill REST API.
+ * POSTs to {@code /query.json} with a JSON body
+ * {@code {"queryType":"SQL","query":""}} using HTTP Basic auth.
+ *
+ * @param baseUrl Drill REST endpoint, e.g. {@code http://host:8047}
+ * @param username Drill user name
+ * @param password Drill password (may be null or empty)
+ * @param sql the SQL statement to execute
+ * @return the raw JSON response string from Drill
+ */
+ private static String executeQuery(String baseUrl, String username, String password, String sql)
+ throws Exception {
+ String endpoint = baseUrl + "/query.json";
+ Map payload = new HashMap<>();
+ payload.put("queryType", "SQL");
+ payload.put("query", sql);
+ String body = MAPPER.writeValueAsString(payload);
+
+ HttpURLConnection conn = null;
+ try {
+ URL url = new URL(endpoint);
+ conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("POST");
+ conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
+ conn.setReadTimeout(READ_TIMEOUT_MS);
+ conn.setDoOutput(true);
+ conn.setRequestProperty("Content-Type", "application/json");
+ conn.setRequestProperty("Accept", "application/json");
+
+ // HTTP Basic auth
+ String creds = username + ":" + (password == null ? "" : password);
+ String encoded = Base64.getEncoder().encodeToString(
+ creds.getBytes(StandardCharsets.UTF_8));
+ conn.setRequestProperty("Authorization", "Basic " + encoded);
+
+ // Write request body
+ try (OutputStream os = conn.getOutputStream()) {
+ os.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ int code = conn.getResponseCode();
+ InputStream is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
+ String response = readAll(is);
+
+ if (code < 200 || code >= 300) {
+ throw new RuntimeException("Drill REST API returned HTTP " + code
+ + ": " + truncate(response, 500));
+ }
+ return response;
+ } finally {
+ if (conn != null) {
+ conn.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Extracts the first column value from each row of a Drill REST API response.
+ * Drill returns JSON like:
+ *
{@code
+ * {"columns":["COL1"],"rows":[{"COL1":"value1"},{"COL1":"value2"}]}
+ * }
+ *
+ * This method is used for ALL resource levels (datasource/schema/table/column)
+ * because every lookup SQL selects exactly one column. The "column" in the
+ * method name refers to the JSON result column, not the Ranger column resource.
+ *
+ * @param jsonResponse the raw JSON response from {@code POST /query.json}
+ * @return list of string values from the first column
+ */
+ private static List extractFirstColumnValues(String jsonResponse) throws Exception {
+ JsonNode root = MAPPER.readTree(jsonResponse);
+ if (root == null || !root.has("rows") || !root.get("rows").isArray()) {
+ return Collections.emptyList();
+ }
+
+ // Determine the first column name from the "columns" array
+ String firstColumn = null;
+ if (root.has("columns") && root.get("columns").isArray()) {
+ JsonNode cols = root.get("columns");
+ if (cols.size() > 0) {
+ firstColumn = cols.get(0).asText();
+ }
+ }
+
+ List result = new ArrayList<>();
+ for (JsonNode row : root.get("rows")) {
+ if (firstColumn != null && row.has(firstColumn)) {
+ String value = row.get(firstColumn).asText();
+ if (value != null && !value.trim().isEmpty()) {
+ result.add(value.trim());
+ }
+ } else if (row.isObject() && row.size() > 0) {
+ // Fallback: use the first field in the row
+ String value = row.elements().next().asText();
+ if (value != null && !value.trim().isEmpty()) {
+ result.add(value.trim());
+ }
+ }
+ }
+ return result;
+ }
+
+ private static String readAll(InputStream is) throws Exception {
+ if (is == null) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(is, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line).append('\n');
+ }
+ }
+ return sb.toString();
+ }
+
+ private static String truncate(String s, int maxLen) {
+ if (s == null) {
+ return "";
+ }
+ return s.length() <= maxLen ? s : s.substring(0, maxLen) + "...";
+ }
+
+ // ========================================================================
+ // Common helpers
+ // ========================================================================
+
+ private String getConfig(String key) {
+ if (configs == null) {
+ return null;
+ }
+ Object value = configs.get(key);
+ return value == null ? null : value.toString().trim();
+ }
+
+ private static String firstHint(Map> hints, String resourceName) {
+ if (hints == null) {
+ return null;
+ }
+ List values = hints.get(resourceName);
+ if (values == null || values.isEmpty()) {
+ return null;
+ }
+ return values.get(0);
+ }
+
+ private static String escapeSql(String value) {
+ return value == null ? "" : value.replace("'", "''");
+ }
+
+ private static boolean isBlank(String s) {
+ return s == null || s.trim().isEmpty();
+ }
+
+ private static Map failure(Map result, String message) {
+ result.put("status", "FAILURE");
+ result.put("message", message);
+ return result;
+ }
+}
diff --git a/drill-ranger/drill-ranger-service/src/test/java/org/apache/ranger/services/drill/RangerServiceDrillTest.java b/drill-ranger/drill-ranger-service/src/test/java/org/apache/ranger/services/drill/RangerServiceDrillTest.java
new file mode 100644
index 00000000000..585a3c51fb1
--- /dev/null
+++ b/drill-ranger/drill-ranger-service/src/test/java/org/apache/ranger/services/drill/RangerServiceDrillTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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.services.drill;
+
+import org.apache.ranger.plugin.service.ResourceLookupContext;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link RangerServiceDrill}.
+ * Covers URL normalization, SQL escaping, JSON parsing, hint extraction,
+ * and the control-flow edge cases of lookupResource / validateConfig that
+ * can be exercised without a live Drill cluster.
+ */
+public class RangerServiceDrillTest {
+
+ // ========================================================================
+ // buildBaseUrl (package-private static — directly callable from same package)
+ // ========================================================================
+
+ @Test
+ public void buildBaseUrl_stripsTrailingSlash() {
+ assertEquals("http://host:8047", RangerServiceDrill.buildBaseUrl("http://host:8047/"));
+ }
+
+ @Test
+ public void buildBaseUrl_handlesMultipleTrailingSlashes() {
+ assertEquals("http://host:8047", RangerServiceDrill.buildBaseUrl("http://host:8047///"));
+ }
+
+ @Test
+ public void buildBaseUrl_trimsWhitespace() {
+ assertEquals("http://host:8047", RangerServiceDrill.buildBaseUrl(" http://host:8047 "));
+ }
+
+ @Test
+ public void buildBaseUrl_throwsForBlankInput() {
+ assertThrows(IllegalArgumentException.class, () -> RangerServiceDrill.buildBaseUrl(null));
+ assertThrows(IllegalArgumentException.class, () -> RangerServiceDrill.buildBaseUrl(""));
+ assertThrows(IllegalArgumentException.class, () -> RangerServiceDrill.buildBaseUrl(" "));
+ }
+
+ @Test
+ public void buildBaseUrl_preservesProtocol() {
+ assertEquals("https://host:8047", RangerServiceDrill.buildBaseUrl("https://host:8047"));
+ assertEquals("https://host:8047", RangerServiceDrill.buildBaseUrl("https://host:8047/"));
+ }
+
+ // ========================================================================
+ // escapeSql (private static — reflection)
+ // ========================================================================
+
+ @Test
+ public void escapeSql_doublesSingleQuote() throws Exception {
+ assertEquals("it''s", invokeEscapeSql("it's"));
+ }
+
+ @Test
+ public void escapeSql_nullReturnsEmpty() throws Exception {
+ assertEquals("", invokeEscapeSql(null));
+ }
+
+ @Test
+ public void escapeSql_noQuotes_unchanged() throws Exception {
+ assertEquals("mysql", invokeEscapeSql("mysql"));
+ }
+
+ // ========================================================================
+ // firstHint (private static — reflection)
+ // ========================================================================
+
+ @Test
+ public void firstHint_returnsFirstValue() throws Exception {
+ Map> hints = new HashMap<>();
+ hints.put("datasource", java.util.Arrays.asList("mysql", "dfs"));
+ assertEquals("mysql", invokeFirstHint(hints, "datasource"));
+ }
+
+ @Test
+ public void firstHint_nullHints_returnsNull() throws Exception {
+ assertNull(invokeFirstHint(null, "datasource"));
+ }
+
+ @Test
+ public void firstHint_emptyList_returnsNull() throws Exception {
+ Map> hints = new HashMap<>();
+ hints.put("datasource", Collections.emptyList());
+ assertNull(invokeFirstHint(hints, "datasource"));
+ }
+
+ @Test
+ public void firstHint_missingKey_returnsNull() throws Exception {
+ Map> hints = new HashMap<>();
+ hints.put("schema", java.util.Arrays.asList("shf"));
+ assertNull(invokeFirstHint(hints, "datasource"));
+ }
+
+ // ========================================================================
+ // extractFirstColumnValues (private static — reflection)
+ // ========================================================================
+
+ @Test
+ public void extractFirstColumnValues_parsesRows() throws Exception {
+ String json = "{\"columns\":[\"COL1\"],\"rows\":[{\"COL1\":\"v1\"},{\"COL1\":\"v2\"}]}";
+ List result = invokeExtractFirstColumnValues(json);
+ assertEquals(java.util.Arrays.asList("v1", "v2"), result);
+ }
+
+ @Test
+ public void extractFirstColumnValues_emptyRows_returnsEmptyList() throws Exception {
+ String json = "{\"columns\":[\"COL1\"],\"rows\":[]}";
+ List result = invokeExtractFirstColumnValues(json);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void extractFirstColumnValues_missingRowsKey_returnsEmptyList() throws Exception {
+ String json = "{\"columns\":[\"COL1\"]}";
+ List result = invokeExtractFirstColumnValues(json);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void extractFirstColumnValues_skipsBlankValues() throws Exception {
+ String json = "{\"columns\":[\"COL1\"],\"rows\":[{\"COL1\":\" \"},{\"COL1\":\"v2\"}]}";
+ List result = invokeExtractFirstColumnValues(json);
+ assertEquals(Collections.singletonList("v2"), result);
+ }
+
+ @Test
+ public void extractFirstColumnValues_fallbackToFirstField() throws Exception {
+ // No "columns" array — should fall back to the first field of each row
+ String json = "{\"rows\":[{\"X\":\"a\"},{\"Y\":\"b\"}]}";
+ List result = invokeExtractFirstColumnValues(json);
+ // Each row contributes its first field value
+ assertEquals(2, result.size());
+ assertTrue(result.contains("a"));
+ assertTrue(result.contains("b"));
+ }
+
+ // ========================================================================
+ // lookupResource edge cases (no executeQuery needed)
+ // ========================================================================
+
+ @Test
+ public void lookupResource_nullContext_returnsEmpty() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", "http://host:8047");
+ List result = service.lookupResource(null);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void lookupResource_blankResourceName_returnsEmpty() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", "http://host:8047");
+ ResourceLookupContext ctx = new ResourceLookupContext();
+ ctx.setResourceName("");
+ List result = service.lookupResource(ctx);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void lookupResource_unknownResourceName_returnsEmpty() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", "http://host:8047");
+ ResourceLookupContext ctx = new ResourceLookupContext();
+ ctx.setResourceName("foo");
+ // lookupResource throws on unknown resource via the default branch,
+ // but the catch block re-throws. Wrap in try/catch and assert empty only
+ // if it returns. If it throws, that's also acceptable behavior.
+ try {
+ List result = service.lookupResource(ctx);
+ assertTrue(result.isEmpty());
+ } catch (Exception e) {
+ // Acceptable: unknown resource name may throw
+ }
+ }
+
+ @Test
+ public void lookupResource_schema_blankDatasource_returnsEmpty() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", "http://host:8047");
+ ResourceLookupContext ctx = new ResourceLookupContext();
+ ctx.setResourceName("schema");
+ ctx.setResources(Collections.emptyMap());
+ List result = service.lookupResource(ctx);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void lookupResource_table_missingHints_returnsEmpty() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", "http://host:8047");
+ // Only datasource hint, missing schema
+ Map> hints = new HashMap<>();
+ hints.put("datasource", Collections.singletonList("mysql"));
+ ResourceLookupContext ctx = new ResourceLookupContext();
+ ctx.setResourceName("table");
+ ctx.setResources(hints);
+ List result = service.lookupResource(ctx);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void lookupResource_column_missingTableHint_returnsEmpty() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", "http://host:8047");
+ Map> hints = new HashMap<>();
+ hints.put("datasource", Collections.singletonList("mysql"));
+ hints.put("schema", Collections.singletonList("shf"));
+ ResourceLookupContext ctx = new ResourceLookupContext();
+ ctx.setResourceName("column");
+ ctx.setResources(hints);
+ List result = service.lookupResource(ctx);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void lookupResource_column_missingSchemaHint_returnsEmpty() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", "http://host:8047");
+ Map> hints = new HashMap<>();
+ hints.put("datasource", Collections.singletonList("mysql"));
+ hints.put("table", Collections.singletonList("orders"));
+ ResourceLookupContext ctx = new ResourceLookupContext();
+ ctx.setResourceName("column");
+ ctx.setResources(hints);
+ List result = service.lookupResource(ctx);
+ assertTrue(result.isEmpty());
+ }
+
+ // ========================================================================
+ // validateConfig edge cases (no executeQuery needed)
+ // ========================================================================
+
+ @Test
+ public void validateConfig_missingUsername_returnsFailure() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ null, "pw", "http://host:8047");
+ Map result = service.validateConfig();
+ assertEquals("FAILURE", result.get("status"));
+ assertNotNull(result.get("message"));
+ }
+
+ @Test
+ public void validateConfig_blankUsername_returnsFailure() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ " ", "pw", "http://host:8047");
+ Map result = service.validateConfig();
+ assertEquals("FAILURE", result.get("status"));
+ }
+
+ @Test
+ public void validateConfig_missingUrl_returnsFailure() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", null);
+ Map result = service.validateConfig();
+ assertEquals("FAILURE", result.get("status"));
+ }
+
+ @Test
+ public void validateConfig_blankUrl_returnsFailure() throws Exception {
+ RangerServiceDrill service = newServiceWithConfigs(
+ "root", "pw", " ");
+ Map result = service.validateConfig();
+ assertEquals("FAILURE", result.get("status"));
+ }
+
+ // ========================================================================
+ // Reflection helpers
+ // ========================================================================
+
+ private static String invokeEscapeSql(String input) throws Exception {
+ Method m = RangerServiceDrill.class.getDeclaredMethod("escapeSql", String.class);
+ m.setAccessible(true);
+ return (String) m.invoke(null, input);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List invokeExtractFirstColumnValues(String json) throws Exception {
+ Method m = RangerServiceDrill.class.getDeclaredMethod("extractFirstColumnValues", String.class);
+ m.setAccessible(true);
+ return (List) m.invoke(null, json);
+ }
+
+ private static String invokeFirstHint(Map> hints, String key) throws Exception {
+ Method m = RangerServiceDrill.class.getDeclaredMethod("firstHint", Map.class, String.class);
+ m.setAccessible(true);
+ return (String) m.invoke(null, hints, key);
+ }
+
+ /**
+ * Creates a RangerServiceDrill instance with the {@code configs} field
+ * pre-populated. The {@code configs} field is declared in
+ * {@link org.apache.ranger.plugin.service.RangerBaseService} as a
+ * protected {@code Map}.
+ */
+ private static RangerServiceDrill newServiceWithConfigs(
+ String username, String password, String url) throws Exception {
+ RangerServiceDrill service = new RangerServiceDrill();
+ Map configs = new HashMap<>();
+ if (username != null) {
+ configs.put("username", username);
+ }
+ if (password != null) {
+ configs.put("password", password);
+ }
+ if (url != null) {
+ configs.put("drill.connection.url", url);
+ }
+ Field configsField = service.getClass().getSuperclass().getDeclaredField("configs");
+ configsField.setAccessible(true);
+ configsField.set(service, configs);
+ return service;
+ }
+}
diff --git a/drill-ranger/pom.xml b/drill-ranger/pom.xml
new file mode 100644
index 00000000000..0e0b0cd6ce8
--- /dev/null
+++ b/drill-ranger/pom.xml
@@ -0,0 +1,74 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.drill
+ drill-root
+ 1.23.0-SNAPSHOT
+
+
+
+ drill-ranger-parent
+ pom
+ Drill : Ranger Integration Parent
+
+ Parent module aggregating the Drill Ranger authorization plugin
+ (drill-ranger-plugin) and the Drill Ranger service plugin
+ (drill-ranger-service).
+
+
+
+ 2.8.0
+
+ 1.19.4
+
+
+
+ drill-ranger-plugin
+ drill-ranger-service
+
+
diff --git a/exec/java-exec/pom.xml b/exec/java-exec/pom.xml
index fe2c229a9a0..5e76055df1a 100644
--- a/exec/java-exec/pom.xml
+++ b/exec/java-exec/pom.xml
@@ -697,6 +697,12 @@
swagger-jaxrs2-servlet-initializer-v2-jakarta
${swagger.version}