You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by ma...@apache.org on 2026/04/01 18:52:37 UTC
(ranger) branch master updated: RANGER-5371: pdp server implementation (#899)
This is an automated email from the ASF dual-hosted git repository.
madhan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git
The following commit(s) were added to refs/heads/master by this push:
new e4c0a6628 RANGER-5371: pdp server implementation (#899)
e4c0a6628 is described below
commit e4c0a6628d79e917f177071fcc465e789f08e382
Author: Madhan Neethiraj <ma...@apache.org>
AuthorDate: Wed Apr 1 11:52:32 2026 -0700
RANGER-5371: pdp server implementation (#899)
* RANGER-5371: pdp server implementation
* RANGER-5531: update Python client to support Ranger PDP server APIs
---
.../ranger/authz/model/RangerAuthzResult.java | 13 +-
.../ranger/authz/model/RangerMultiAuthzResult.java | 13 +-
.../ranger/authz/embedded/RangerAuthzPlugin.java | 8 +-
.../authz/embedded/RangerEmbeddedAuthorizer.java | 32 +-
dev-support/ranger-docker/.dockerignore | 1 +
dev-support/ranger-docker/.env | 4 +
dev-support/ranger-docker/Dockerfile.ranger-pdp | 36 ++
dev-support/ranger-docker/README.md | 8 +-
.../ranger-docker/docker-compose.ranger-pdp.yml | 61 +++
.../ranger-docker/scripts/kdc/entrypoint.sh | 5 +-
dev-support/ranger-docker/scripts/pdp/logback.xml | 47 ++
.../ranger-docker/scripts/pdp/ranger-pdp-site.xml | 240 ++++++++++
.../ranger-docker/scripts/pdp/ranger-pdp.sh | 39 ++
distro/pom.xml | 8 +
distro/src/main/assembly/pdp.xml | 207 +++++++++
intg/src/main/python/README.md | 470 +++++++++++++++-----
.../main/python/apache_ranger/client/__init__.py | 31 ++
.../apache_ranger/client/ranger_pdp_client.py | 84 ++++
intg/src/main/python/apache_ranger/exceptions.py | 22 +-
.../main/python/apache_ranger/model/__init__.py | 19 +
.../python/apache_ranger/model/ranger_authz.py | 260 +++++++++++
pdp/conf.dist/README-k8s.md | 103 +++++
pdp/conf.dist/logback.xml | 36 ++
pdp/conf.dist/ranger-pdp-site.xml | 364 +++++++++++++++
pdp/pom.xml | 200 +++++++++
pdp/scripts/ranger-pdp-services.sh | 223 ++++++++++
pdp/scripts/ranger-pdp.sh | 76 ++++
.../org/apache/ranger/pdp/RangerPdpServer.java | 295 +++++++++++++
.../java/org/apache/ranger/pdp/RangerPdpStats.java | 113 +++++
.../apache/ranger/pdp/RangerPdpStatusServlet.java | 129 ++++++
.../apache/ranger/pdp/config/RangerPdpConfig.java | 281 ++++++++++++
.../ranger/pdp/config/RangerPdpConstants.java | 94 ++++
.../org/apache/ranger/pdp/model/ErrorResponse.java | 45 ++
.../ranger/pdp/rest/RangerPdpApplication.java | 62 +++
.../org/apache/ranger/pdp/rest/RangerPdpREST.java | 491 +++++++++++++++++++++
.../pdp/security/HttpHeaderAuthNHandler.java | 71 +++
.../ranger/pdp/security/JwtAuthNHandler.java | 105 +++++
.../ranger/pdp/security/KerberosAuthNHandler.java | 275 ++++++++++++
.../ranger/pdp/security/PdpAuthNHandler.java | 111 +++++
.../ranger/pdp/security/RangerPdpAuthNFilter.java | 189 ++++++++
.../security/RangerPdpRequestContextFilter.java | 73 +++
pdp/src/main/resources/ranger-pdp-default.xml | 364 +++++++++++++++
.../org/apache/ranger/pdp/RangerPdpStatsTest.java | 54 +++
.../ranger/pdp/RangerPdpStatusServletTest.java | 108 +++++
.../ranger/pdp/config/RangerPdpConfigTest.java | 51 +++
.../apache/ranger/pdp/rest/RangerPdpRESTTest.java | 360 +++++++++++++++
.../pdp/security/HttpHeaderAuthNHandlerTest.java | 84 ++++
.../pdp/security/KerberosAuthHandlerTest.java | 69 +++
.../pdp/security/RangerPdpAuthNFilterTest.java | 99 +++++
.../RangerPdpRequestContextFilterTest.java | 132 ++++++
pom.xml | 3 +
.../src/main/python/sample_kms_client.py | 86 ++++
.../src/main/python/sample_pdp_client.py | 101 +++++
53 files changed, 6311 insertions(+), 144 deletions(-)
diff --git a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java
index 4c6470660..0dbf6b4cb 100644
--- a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java
+++ b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerAuthzResult.java
@@ -41,7 +41,12 @@ public RangerAuthzResult() {
}
public RangerAuthzResult(String requestId) {
- this(requestId, null);
+ this.requestId = requestId;
+ }
+
+ public RangerAuthzResult(String requestId, AccessDecision decision) {
+ this.requestId = requestId;
+ this.decision = decision;
}
public RangerAuthzResult(String requestId, Map<String, PermissionResult> permissions) {
@@ -49,6 +54,12 @@ public RangerAuthzResult(String requestId, Map<String, PermissionResult> permiss
this.permissions = permissions;
}
+ public RangerAuthzResult(String requestId, AccessDecision decision, Map<String, PermissionResult> permissions) {
+ this.requestId = requestId;
+ this.decision = decision;
+ this.permissions = permissions;
+ }
+
public String getRequestId() {
return requestId;
}
diff --git a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerMultiAuthzResult.java b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerMultiAuthzResult.java
index 5c15c2fd6..cae5d213e 100644
--- a/authz-api/src/main/java/org/apache/ranger/authz/model/RangerMultiAuthzResult.java
+++ b/authz-api/src/main/java/org/apache/ranger/authz/model/RangerMultiAuthzResult.java
@@ -39,7 +39,12 @@ public RangerMultiAuthzResult() {
}
public RangerMultiAuthzResult(String requestId) {
- this(requestId, null);
+ this.requestId = requestId;
+ }
+
+ public RangerMultiAuthzResult(String requestId, AccessDecision decision) {
+ this.requestId = requestId;
+ this.decision = decision;
}
public RangerMultiAuthzResult(String requestId, List<RangerAuthzResult> accesses) {
@@ -47,6 +52,12 @@ public RangerMultiAuthzResult(String requestId, List<RangerAuthzResult> accesses
this.accesses = accesses;
}
+ public RangerMultiAuthzResult(String requestId, AccessDecision decision, List<RangerAuthzResult> accesses) {
+ this.requestId = requestId;
+ this.decision = decision;
+ this.accesses = accesses;
+ }
+
public String getRequestId() {
return requestId;
}
diff --git a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java
index 3fd84a033..4f0be2a5d 100644
--- a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java
+++ b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerAuthzPlugin.java
@@ -70,8 +70,8 @@ class RangerAuthzPlugin {
private final RangerBasePlugin plugin;
private final Map<String, RangerResourceNameParser> rrnTemplates = new HashMap<>();
- public RangerAuthzPlugin(String serviceType, String serviceName, Properties properties) {
- plugin = new RangerBasePlugin(getPluginConfig(serviceType, serviceName, properties)) {
+ public RangerAuthzPlugin(String serviceType, String serviceName, String appId, Properties properties) {
+ plugin = new RangerBasePlugin(getPluginConfig(serviceType, serviceName, appId, properties)) {
@Override
public void setPolicies(ServicePolicies policies) {
super.setPolicies(policies);
@@ -407,7 +407,7 @@ private void updateResourceTemplates() {
}
}
- private static RangerPluginConfig getPluginConfig(String serviceType, String serviceName, Properties properties) {
- return new RangerPluginConfig(serviceType, serviceName, null, properties);
+ private static RangerPluginConfig getPluginConfig(String serviceType, String serviceName, String appId, Properties properties) {
+ return new RangerPluginConfig(serviceType, serviceName, appId, properties);
}
}
diff --git a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerEmbeddedAuthorizer.java b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerEmbeddedAuthorizer.java
index 650c16187..d2d8d570b 100644
--- a/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerEmbeddedAuthorizer.java
+++ b/authz-embedded/src/main/java/org/apache/ranger/authz/embedded/RangerEmbeddedAuthorizer.java
@@ -36,9 +36,11 @@
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import static org.apache.ranger.authz.api.RangerAuthzApiErrorCode.INVALID_REQUEST_SERVICE_NAME_OR_TYPE_MANDATORY;
import static org.apache.ranger.authz.embedded.RangerEmbeddedAuthzErrorCode.NO_DEFAULT_SERVICE_FOR_SERVICE_TYPE;
@@ -49,7 +51,7 @@ public class RangerEmbeddedAuthorizer extends RangerAuthorizer {
private final RangerAuthzConfig config;
private final String appType;
- private final Map<String, RangerAuthzPlugin> plugins = new HashMap<>();
+ private final Map<String, RangerAuthzPlugin> plugins = new ConcurrentHashMap<>();
public RangerEmbeddedAuthorizer(Properties properties) {
super(properties);
@@ -131,6 +133,10 @@ public RangerMultiAuthzResult authorize(RangerMultiAuthzRequest request, RangerA
return authorize(request, plugin, auditHandler);
}
+ public Set<String> getLoadedServices() {
+ return new HashSet<>(this.plugins.keySet());
+ }
+
@Override
protected void validateAccessContext(RangerAccessContext context) throws RangerAuthzException {
super.validateAccessContext(context);
@@ -206,24 +212,18 @@ private RangerMultiAuthzResult authorize(RangerMultiAuthzRequest request, Ranger
return result;
}
- private RangerAuthzPlugin getOrCreatePlugin(String serviceName, String serviceType) throws RangerAuthzException {
- RangerAuthzPlugin ret = plugins.get(serviceName);
-
- if (ret == null) {
- synchronized (plugins) {
- ret = plugins.get(serviceName);
+ private RangerAuthzPlugin getOrCreatePlugin(String serviceName, String serviceType) {
+ return plugins.computeIfAbsent(serviceName, k -> createPlugin(serviceName, serviceType));
+ }
- if (ret == null) {
- Properties pluginProperties = this.config.getServiceProperties(serviceName, serviceType);
+ private RangerAuthzPlugin createPlugin(String serviceName, String serviceType) {
+ Properties pluginProperties = config.getServiceProperties(serviceName, serviceType);
- LOG.debug("properties for service {}: {}", serviceName, pluginProperties);
+ LOG.debug("properties for service {}: {}", serviceName, pluginProperties);
- ret = new RangerAuthzPlugin(serviceType, serviceName, pluginProperties);
+ RangerAuthzPlugin ret = new RangerAuthzPlugin(serviceType, serviceName, appType, pluginProperties);
- plugins.put(serviceName, ret);
- }
- }
- }
+ LOG.info("Created plugin: serviceName={}, serviceType={}", serviceName, serviceType);
return ret;
}
diff --git a/dev-support/ranger-docker/.dockerignore b/dev-support/ranger-docker/.dockerignore
index 22e3ff3d9..6e66a678e 100644
--- a/dev-support/ranger-docker/.dockerignore
+++ b/dev-support/ranger-docker/.dockerignore
@@ -5,6 +5,7 @@
!dist/ranger-*-kms.tar.gz
!dist/ranger-*-usersync.tar.gz
!dist/ranger-*-tagsync.tar.gz
+!dist/ranger-*-pdp.tar.gz
!dist/ranger-*-audit-server.tar.gz
!dist/ranger-*-audit-consumer-solr.tar.gz
!dist/ranger-*-audit-consumer-hdfs.tar.gz
diff --git a/dev-support/ranger-docker/.env b/dev-support/ranger-docker/.env
index 03a222ec7..ddaa76588 100644
--- a/dev-support/ranger-docker/.env
+++ b/dev-support/ranger-docker/.env
@@ -47,6 +47,9 @@ USERSYNC_VERSION=3.0.0-SNAPSHOT
# Tagsync Configuration
TAGSYNC_VERSION=3.0.0-SNAPSHOT
+# PDP Configuration
+PDP_VERSION=3.0.0-SNAPSHOT
+
# Solr Configuration
SOLR_VERSION=8.11.3
SOLR_PLUGIN_VERSION=3.0.0-SNAPSHOT
@@ -84,4 +87,5 @@ OPENSEARCH_VERSION=1.3.19
DEBUG_ADMIN=false
DEBUG_USERSYNC=false
DEBUG_TAGSYNC=false
+DEBUG_PDP=false
ENABLE_FILE_SYNC_SOURCE=false
diff --git a/dev-support/ranger-docker/Dockerfile.ranger-pdp b/dev-support/ranger-docker/Dockerfile.ranger-pdp
new file mode 100644
index 000000000..0f74b3162
--- /dev/null
+++ b/dev-support/ranger-docker/Dockerfile.ranger-pdp
@@ -0,0 +1,36 @@
+# 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.
+
+ARG RANGER_BASE_IMAGE=apache/ranger-base
+ARG RANGER_BASE_VERSION=20260123-2-8
+
+FROM ${RANGER_BASE_IMAGE}:${RANGER_BASE_VERSION}
+
+ARG PDP_VERSION
+
+COPY ./dist/ranger-${PDP_VERSION}-pdp.tar.gz /home/ranger/dist/
+COPY ./scripts/pdp/ranger-pdp.sh ${RANGER_SCRIPTS}/
+
+RUN tar xvfz /home/ranger/dist/ranger-${PDP_VERSION}-pdp.tar.gz --directory=${RANGER_HOME} \
+ && ln -s ${RANGER_HOME}/ranger-${PDP_VERSION}-pdp ${RANGER_HOME}/pdp \
+ && rm -f /home/ranger/dist/ranger-${PDP_VERSION}-pdp.tar.gz \
+ && mkdir -p /var/log/ranger/pdp /var/run/ranger /etc/ranger/cache \
+ && ln -s ${RANGER_HOME}/pdp/ranger-pdp-services.sh /usr/bin/ranger-pdp-services.sh \
+ && chown -R ranger:ranger ${RANGER_HOME}/pdp/ ${RANGER_SCRIPTS}/ /var/log/ranger/ /var/run/ranger /etc/ranger/ \
+ && chmod 744 ${RANGER_SCRIPTS}/ranger-pdp.sh
+
+USER ranger
+ENTRYPOINT [ "/home/ranger/scripts/ranger-pdp.sh" ]
diff --git a/dev-support/ranger-docker/README.md b/dev-support/ranger-docker/README.md
index 63f206830..b0156d61c 100644
--- a/dev-support/ranger-docker/README.md
+++ b/dev-support/ranger-docker/README.md
@@ -74,14 +74,14 @@ cd dev-support/ranger-docker
### Run Ranger Services in Containers
-#### Bring up ranger-core services: ranger, usersync, tagsync and ranger-kms in containers
+#### Bring up ranger-core services: ranger, usersync, tagsync, pdp and kms in containers
~~~
# To enable file based sync source for usersync do:
# export ENABLE_FILE_SYNC_SOURCE=true
# valid values for RANGER_DB_TYPE: mysql/postgres/oracle
-docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-kms.yml up -d
+docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-pdp.yml -f docker-compose.ranger-kms.yml up -d
# Ranger Admin can be accessed at http://localhost:6080 (admin/rangerR0cks!)
~~~
@@ -111,7 +111,7 @@ Similarly, check the `depends` section of the `docker-compose.ranger-service.yam
#### Bring up all containers
~~~
./scripts/ozone/ozone-plugin-docker-setup.sh
-docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-kms.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-hbase.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-hive.yml -f docker-compose.ranger-knox.yml -f docker-compose.ranger-ozone.yml up -d
+docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-pdp.yml -f docker-compose.ranger-kms.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-hbase.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-hive.yml -f docker-compose.ranger-knox.yml -f docker-compose.ranger-ozone.yml up -d
~~~
#### To rebuild specific images and start containers with the new image:
@@ -122,4 +122,4 @@ docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.ym
#### To bring up audit server, solr and hdfs consumer. Make sure kafka,solr and hdfs containers are running before bring up audit server.
~~~
docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-audit-server.yml up -d
-~~~
\ No newline at end of file
+~~~
diff --git a/dev-support/ranger-docker/docker-compose.ranger-pdp.yml b/dev-support/ranger-docker/docker-compose.ranger-pdp.yml
new file mode 100644
index 000000000..78015f88a
--- /dev/null
+++ b/dev-support/ranger-docker/docker-compose.ranger-pdp.yml
@@ -0,0 +1,61 @@
+# 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.
+
+services:
+ ranger-pdp:
+ build:
+ context: .
+ dockerfile: Dockerfile.ranger-pdp
+ args:
+ - RANGER_BASE_IMAGE=${RANGER_BASE_IMAGE}
+ - RANGER_BASE_VERSION=${RANGER_BASE_VERSION}
+ - PDP_VERSION=${PDP_VERSION}
+ image: ranger-pdp
+ container_name: ranger-pdp
+ hostname: ranger-pdp.rangernw
+ volumes:
+ - ./dist/keytabs/ranger-pdp:/etc/keytabs
+ - ./scripts/kdc/krb5.conf:/etc/krb5.conf
+ - ./scripts/hadoop/core-site.xml:/home/ranger/scripts/core-site.xml:ro
+ - ./dist/version:/home/ranger/dist/version:ro
+ - ./scripts/pdp/logback.xml:/opt/ranger/pdp/conf/logback.xml
+ - ./scripts/pdp/ranger-pdp-site.xml:/opt/ranger/pdp/conf/ranger-pdp-site.xml
+ stdin_open: true
+ tty: true
+ networks:
+ - ranger
+ ports:
+ - "6500:6500"
+ healthcheck:
+ test: ["CMD-SHELL", "curl -sf http://localhost:6500/health/ready >/dev/null || exit 1"]
+ interval: 20s
+ timeout: 10s
+ retries: 20
+ start_period: 30s
+ depends_on:
+ ranger:
+ condition: service_started
+ ranger-solr:
+ condition: service_started
+ environment:
+ - PDP_VERSION
+ - KERBEROS_ENABLED
+ - DEBUG_PDP=${DEBUG_PDP:-false}
+
+networks:
+ ranger:
+ name: rangernw
+ external: true
diff --git a/dev-support/ranger-docker/scripts/kdc/entrypoint.sh b/dev-support/ranger-docker/scripts/kdc/entrypoint.sh
index e920f413e..998bad441 100644
--- a/dev-support/ranger-docker/scripts/kdc/entrypoint.sh
+++ b/dev-support/ranger-docker/scripts/kdc/entrypoint.sh
@@ -90,6 +90,9 @@ function create_keytabs() {
create_principal_and_keytab rangerkms ranger-kms
+ create_principal_and_keytab HTTP ranger-pdp
+ create_principal_and_keytab rangerpdp ranger-pdp
+
create_principal_and_keytab dn ranger-hadoop
create_principal_and_keytab hdfs ranger-hadoop
create_principal_and_keytab healthcheck ranger-hadoop
@@ -142,7 +145,7 @@ if [ ! -f $DB_DIR/principal ]; then
echo "Database initialized"
create_keytabs
- create_testusers ranger ranger-usersync ranger-tagsync ranger-audit-server ranger-audit-consumer-solr ranger-audit-consumer-hdfs ranger-hadoop ranger-hive ranger-hbase ranger-kafka ranger-solr ranger-knox ranger-kms ranger-ozone ranger-trino ranger-opensearch
+ create_testusers ranger ranger-usersync ranger-tagsync ranger-pdp ranger-audit-server ranger-audit-consumer-solr ranger-audit-consumer-hdfs ranger-hadoop ranger-hive ranger-hbase ranger-kafka ranger-solr ranger-knox ranger-kms ranger-ozone ranger-trino ranger-opensearch
else
echo "KDC DB already exists; skipping create"
fi
diff --git a/dev-support/ranger-docker/scripts/pdp/logback.xml b/dev-support/ranger-docker/scripts/pdp/logback.xml
new file mode 100644
index 000000000..769a3e032
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/pdp/logback.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<configuration>
+ <property name="pdpLogDir" value="${logdir:-/var/log/ranger/pdp}"/>
+ <property name="pdpHostname" value="${hostname:-unknown-host}"/>
+
+ <appender name="file" class="ch.qos.logback.core.FileAppender">
+ <file>${pdpLogDir}/ranger-pdp-${pdpHostname}.log</file>
+ <append>true</append>
+ <encoder>
+ <pattern>%d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n</pattern>
+ </encoder>
+ </appender>
+
+ <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
+ <target>System.out</target>
+ <encoder>
+ <pattern>%d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="com.sun.jersey.server.wadl.generators.WadlGeneratorJAXBGrammarGenerator" level="OFF"/>
+ <logger name="org.apache.directory.server.core" level="OFF"/>
+ <logger name="org.apache.hadoop.security" level="OFF"/>
+ <logger name="org.apache.hadoop.conf" level="ERROR"/>
+ <logger name="org.apache.hadoop.util.NativeCodeLoader" level="OFF"/>
+
+ <root level="INFO">
+ <appender-ref ref="file"/>
+ <appender-ref ref="stdout"/>
+ </root>
+</configuration>
diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
new file mode 100644
index 000000000..3fc4e6245
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml
@@ -0,0 +1,240 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+ 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.
+-->
+<!-- Ranger PDP Server - Docker Configuration -->
+<configuration>
+ <property>
+ <name>ranger.pdp.port</name>
+ <value>6500</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.log.dir</name>
+ <value>/var/log/ranger/pdp</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.service.*.delegation.users</name>
+ <value>ranger</value>
+ </property>
+
+ <!-- Only delegation users are allowed call on behalf of other users -->
+ <property>
+ <name>ranger.pdp.service.dev_hdfs.delegation.users</name>
+ <value>hdfs</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.service.dev_hive.delegation.users</name>
+ <value>hive</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.service.dev_hbase.delegation.users</name>
+ <value>hbase</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.service.dev_kafka.delegation.users</name>
+ <value>kafka</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.service.dev_trino.delegation.users</name>
+ <value>trino</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.service.dev_polaris.delegation.users</name>
+ <value>polaris</value>
+ </property>
+
+ <!-- SSL/TLS: disabled for Docker dev environment -->
+ <property>
+ <name>ranger.pdp.ssl.enabled</name>
+ <value>false</value>
+ </property>
+
+ <!-- HTTP/2 -->
+ <property>
+ <name>ranger.pdp.http2.enabled</name>
+ <value>true</value>
+ </property>
+
+ <!-- Connector concurrency / queue limits -->
+ <property>
+ <name>ranger.pdp.http.connector.maxThreads</name>
+ <value>200</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.minSpareThreads</name>
+ <value>20</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.acceptCount</name>
+ <value>100</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.maxConnections</name>
+ <value>10000</value>
+ </property>
+
+ <!-- Authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.types</name>
+ <value>header,jwt,kerberos</value>
+ </property>
+
+ <!-- HTTP Header authentication -->
+ <property>
+ <name>ranger.pdp.authn.header.enabled</name>
+ <value>false</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.header.username</name>
+ <value>X-Forwarded-User</value>
+ </property>
+
+ <!-- JWT bearer token authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.jwt.enabled</name>
+ <value>false</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.provider.url</name>
+ <value/>
+ </property>
+
+ <!-- Kerberos / SPNEGO authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.kerberos.enabled</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.spnego.principal</name>
+ <value>HTTP/ranger-pdp.rangernw@EXAMPLE.COM</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.spnego.keytab</name>
+ <value>/etc/keytabs/HTTP.keytab</value>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.name.rules</name>
+ <value>DEFAULT</value>
+ </property>
+
+ <!-- Connection to Ranger Admin Server -->
+ <property>
+ <name>ranger.authz.default.policy.rest.url</name>
+ <value>http://ranger:6080</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.client.username</name>
+ <value>admin</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.client.password</name>
+ <value>rangerR0cks!</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.cache.dir</name>
+ <value>/etc/ranger/cache</value>
+ </property>
+
+ <!-- Audit logs -->
+ <property>
+ <name>ranger.authz.audit.is.enabled</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.log.status.log.enabled</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.solr</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.solr.urls</name>
+ <value>http://ranger-solr:8983/solr/ranger_audits</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.solr.batch.filespool.dir</name>
+ <value>/var/log/ranger/pdp/audit/solr/spool</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.solr.force.use.inmemory.jaas.config</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.jaas.Client.loginModuleName</name>
+ <value>com.sun.security.auth.module.Krb5LoginModule</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.jaas.Client.loginModuleControlFlag</name>
+ <value>required</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.jaas.Client.option.serviceName</name>
+ <value>HTTP</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.jaas.Client.option.useKeyTab</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.jaas.Client.option.storeKey</name>
+ <value>false</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.jaas.Client.option.useTicketCache</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.jaas.Client.option.keyTab</name>
+ <value>/etc/keytabs/rangerpdp.keytab</value>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.jaas.Client.option.principal</name>
+ <value>rangerpdp/ranger-pdp.rangernw@EXAMPLE.COM</value>
+ </property>
+</configuration>
diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp.sh b/dev-support/ranger-docker/scripts/pdp/ranger-pdp.sh
new file mode 100644
index 000000000..89e2f3c09
--- /dev/null
+++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+# 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.
+
+if [ ! -e ${RANGER_HOME}/.setupDone ]
+then
+ SETUP_RANGER=true
+else
+ SETUP_RANGER=false
+fi
+
+if [ "${SETUP_RANGER}" == "true" ]
+then
+ if [ "${KERBEROS_ENABLED}" == "true" ]
+ then
+ ${RANGER_SCRIPTS}/wait_for_keytab.sh HTTP.keytab
+ ${RANGER_SCRIPTS}/wait_for_testusers_keytab.sh
+ cp ${RANGER_SCRIPTS}/core-site.xml ${RANGER_HOME}/pdp/conf/core-site.xml
+ fi
+
+ touch "${RANGER_HOME}"/.setupDone
+fi
+
+cd ${RANGER_HOME}/pdp || exit 1
+exec ./ranger-pdp-services.sh run
diff --git a/distro/pom.xml b/distro/pom.xml
index cb3c09a8c..be243be0c 100644
--- a/distro/pom.xml
+++ b/distro/pom.xml
@@ -297,6 +297,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.ranger</groupId>
+ <artifactId>ranger-pdp</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.ranger</groupId>
<artifactId>ranger-plugin-classloader</artifactId>
@@ -489,6 +495,7 @@
<descriptor>src/main/assembly/tagsync.xml</descriptor>
<descriptor>src/main/assembly/migration-util.xml</descriptor>
<descriptor>src/main/assembly/kms.xml</descriptor>
+ <descriptor>src/main/assembly/pdp.xml</descriptor>
<descriptor>src/main/assembly/ranger-tools.xml</descriptor>
<descriptor>src/main/assembly/ranger-src.xml</descriptor>
<descriptor>src/main/assembly/plugin-atlas.xml</descriptor>
@@ -1077,6 +1084,7 @@
<descriptor>src/main/assembly/tagsync.xml</descriptor>
<descriptor>src/main/assembly/migration-util.xml</descriptor>
<descriptor>src/main/assembly/kms.xml</descriptor>
+ <descriptor>src/main/assembly/pdp.xml</descriptor>
<descriptor>src/main/assembly/ranger-tools.xml</descriptor>
<descriptor>src/main/assembly/ranger-src.xml</descriptor>
<descriptor>src/main/assembly/plugin-atlas.xml</descriptor>
diff --git a/distro/src/main/assembly/pdp.xml b/distro/src/main/assembly/pdp.xml
new file mode 100644
index 000000000..f995fa37e
--- /dev/null
+++ b/distro/src/main/assembly/pdp.xml
@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<assembly>
+ <id>pdp</id>
+ <formats>
+ <format>tar.gz</format>
+ </formats>
+ <baseDirectory>${project.parent.name}-${project.version}-pdp</baseDirectory>
+ <includeBaseDirectory>true</includeBaseDirectory>
+
+ <moduleSets>
+ <!-- ranger-pdp main JAR → dist/; selected runtime dependencies → lib/ -->
+ <moduleSet>
+ <useAllReactorProjects>true</useAllReactorProjects>
+ <includes>
+ <include>org.apache.ranger:ranger-pdp</include>
+ <include>org.apache.ranger:credentialbuilder</include>
+ <include>org.apache.ranger:authz-embedded</include>
+ <include>org.apache.ranger:ranger-audit-core</include>
+ <include>org.apache.ranger:ranger-audit-dest-hdfs</include>
+ <include>org.apache.ranger:ranger-audit-dest-solr</include>
+ <include>org.apache.ranger:ranger-authn</include>
+ <include>org.apache.ranger:ranger-authz-api</include>
+ <include>org.apache.ranger:ranger-common-utils</include>
+ <include>org.apache.ranger:ranger-plugin-classloader</include>
+ <include>org.apache.ranger:ranger-plugins-common</include>
+ <include>org.apache.ranger:ranger-plugins-cred</include>
+ <include>org.apache.ranger:ugsync-util</include>
+ </includes>
+ <binaries>
+ <outputDirectory>lib</outputDirectory>
+ <includeDependencies>true</includeDependencies>
+ <unpack>false</unpack>
+ <directoryMode>755</directoryMode>
+ <fileMode>644</fileMode>
+ <includes>
+ <!-- Embedded Tomcat (HTTP/1.1 + HTTP/2) -->
+ <include>org.apache.tomcat.embed:tomcat-embed-core:jar:${tomcat.embed.version}</include>
+ <include>org.apache.tomcat.embed:tomcat-embed-jasper:jar:${tomcat.embed.version}</include>
+ <include>org.apache.tomcat.embed:tomcat-embed-el:jar:${tomcat.embed.version}</include>
+ <include>org.apache.tomcat:tomcat-annotations-api:jar:${tomcat.embed.version}</include>
+
+ <!-- Jersey 2.x (JAX-RS server + Jackson media type) -->
+ <include>org.glassfish.jersey.core:jersey-server</include>
+ <include>org.glassfish.jersey.core:jersey-common</include>
+ <include>org.glassfish.jersey.core:jersey-client</include>
+ <include>org.glassfish.jersey.containers:jersey-container-servlet-core</include>
+ <include>org.glassfish.jersey.ext:jersey-entity-filtering</include>
+ <include>org.glassfish.jersey.inject:jersey-hk2</include>
+ <include>org.glassfish.jersey.media:jersey-media-json-jackson</include>
+ <include>jakarta.ws.rs:jakarta.ws.rs-api</include>
+ <include>javax.inject:javax.inject</include>
+ <include>javax.validation:validation-api</include>
+
+ <!--
+ Jersey-1 client runtime for ranger-plugins-common.
+ Use client/core only; avoid jersey-bundle to prevent
+ ASM package clashes with Jersey-2 server scanner.
+ -->
+ <include>com.sun.jersey:jersey-client:jar:${jersey-bundle.version}</include>
+ <include>com.sun.jersey:jersey-core:jar:${jersey-bundle.version}</include>
+
+ <!-- HK2 (Jersey DI framework) -->
+ <include>org.glassfish.hk2:hk2-api</include>
+ <include>org.glassfish.hk2:hk2-locator</include>
+ <include>org.glassfish.hk2:hk2-utils</include>
+ <include>org.glassfish.hk2.external:aopalliance-repackaged</include>
+ <include>org.glassfish.hk2.external:javax.inject</include>
+ <include>org.glassfish:osgi-resource-locator</include>
+
+ <!-- Jackson (JSON serialization for authz-api model classes) -->
+ <include>com.fasterxml.jackson.core:jackson-databind:jar:${fasterxml.jackson.databind.version}</include>
+ <include>com.fasterxml.jackson.core:jackson-core:jar:${fasterxml.jackson.version}</include>
+ <include>com.fasterxml.jackson.core:jackson-annotations:jar:${fasterxml.jackson.version}</include>
+ <include>com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:jar:${fasterxml.jackson.version}</include>
+ <include>com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:jar:${fasterxml.jackson.version}</include>
+ <include>com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:${fasterxml.jackson.version}</include>
+
+ <!-- JWT bearer token authentication -->
+ <include>com.nimbusds:nimbus-jose-jwt:jar:${nimbus-jose-jwt.version}</include>
+ <include>net.minidev:json-smart</include>
+ <include>com.nimbusds:content-type</include>
+
+ <!-- Commons -->
+ <include>org.apache.commons:commons-lang3:jar:${commons.lang3.version}</include>
+ <include>commons-codec:commons-codec:jar:${commons.codec.version}</include>
+ <include>org.apache.commons:commons-collections4:jar:${commons.collections4.version}</include>
+ <include>commons-collections:commons-collections:jar:${commons.collections.version}</include>
+ <include>org.apache.commons:commons-configuration2:jar:${commons.configuration.version}</include>
+ <include>org.apache.commons:commons-compress:jar:${commons.compress.version}</include>
+ <include>commons-io:commons-io:jar:${commons.io.version}</include>
+ <include>commons-logging:commons-logging:jar:${commons.logging.version}</include>
+ <include>org.apache.commons:commons-text:jar:${commons.text.version}</include>
+ <include>org.eclipse.jetty:jetty-client:jar:${jetty-client.version}</include>
+
+ <!-- Hadoop client (audit, Kerberos utilities) -->
+ <include>org.apache.hadoop:hadoop-auth:jar:${hadoop.version}</include>
+ <include>org.apache.hadoop:hadoop-client-api:jar:${hadoop.version}</include>
+ <include>org.apache.hadoop:hadoop-client-runtime:jar:${hadoop.version}</include>
+ <include>org.apache.hadoop.thirdparty:hadoop-shaded-guava</include>
+
+ <!-- XML / StAX -->
+ <include>org.codehaus.woodstox:stax2-api:jar:${codehaus.woodstox.stax2api.version}</include>
+ <include>com.fasterxml.woodstox:woodstox-core:jar:${fasterxml.woodstox.version}</include>
+
+ <!-- Audit sinks -->
+ <include>org.apache.solr:solr-solrj</include>
+ <include>org.apache.httpcomponents:httpmime:jar:${httpcomponents.httpmime.version}</include>
+ <include>org.apache.httpcomponents:httpclient:jar:${httpcomponents.httpclient.version}</include>
+ <include>org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version}</include>
+ <include>org.apache.zookeeper:zookeeper:jar:${zookeeper.version}</include>
+ <include>org.noggit:noggit</include>
+
+ <!-- Native libs (hostname lookup) -->
+ <include>com.kstruct:gethostname4j</include>
+ <include>net.java.dev.jna:jna</include>
+ <include>net.java.dev.jna:jna-platform</include>
+
+ <!-- Logging -->
+ <include>org.slf4j:slf4j-api:jar:${slf4j.version}</include>
+ <include>ch.qos.logback:logback-classic:jar:${logback.version}</include>
+ <include>ch.qos.logback:logback-core:jar:${logback.version}</include>
+ </includes>
+ <excludes>
+ <!-- avoid jersey-bundle because it conflicts with Jersey-2 -->
+ <exclude>com.sun.jersey:jersey-bundle</exclude>
+ </excludes>
+ </binaries>
+ </moduleSet>
+
+ </moduleSets>
+
+ <fileSets>
+ <!-- Default config template (copy to conf/ and customise) -->
+ <fileSet>
+ <directoryMode>755</directoryMode>
+ <fileMode>600</fileMode>
+ <outputDirectory>conf.dist</outputDirectory>
+ <directory>${project.parent.basedir}/pdp/conf.dist</directory>
+ </fileSet>
+
+ <!-- Empty runtime config dir (populated by installer / operator) -->
+ <fileSet>
+ <directoryMode>750</directoryMode>
+ <fileMode>640</fileMode>
+ <outputDirectory>conf</outputDirectory>
+ <directory>${project.parent.basedir}/pdp/conf.dist</directory>
+ <excludes>
+ <exclude>**/*</exclude>
+ </excludes>
+ </fileSet>
+
+ <!-- Empty logs dir -->
+ <fileSet>
+ <directoryMode>755</directoryMode>
+ <outputDirectory>logs</outputDirectory>
+ <directory>${project.parent.basedir}/pdp/conf.dist</directory>
+ <excludes>
+ <exclude>**/*</exclude>
+ </excludes>
+ </fileSet>
+
+ <!-- Version file (generated by distro build) -->
+ <fileSet>
+ <directoryMode>755</directoryMode>
+ <outputDirectory></outputDirectory>
+ <directory>${project.build.directory}</directory>
+ <includes>
+ <include>version</include>
+ </includes>
+ <fileMode>444</fileMode>
+ </fileSet>
+ </fileSets>
+
+ <files>
+ <!-- Service control script (placed at root; symlinked to /usr/bin during install) -->
+ <file>
+ <source>${project.parent.basedir}/pdp/scripts/ranger-pdp-services.sh</source>
+ <outputDirectory></outputDirectory>
+ <destName>ranger-pdp-services.sh</destName>
+ <fileMode>755</fileMode>
+ </file>
+
+ <!-- Init-style wrapper (delegates to ranger-pdp-services.sh via su) -->
+ <file>
+ <source>${project.parent.basedir}/pdp/scripts/ranger-pdp.sh</source>
+ <outputDirectory></outputDirectory>
+ <destName>ranger-pdp</destName>
+ <fileMode>755</fileMode>
+ </file>
+ </files>
+</assembly>
diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md
index 90e8ff6b8..a8c0c2671 100644
--- a/intg/src/main/python/README.md
+++ b/intg/src/main/python/README.md
@@ -39,6 +39,173 @@ Package Version
apache-ranger 0.0.12
```
+## Python API clients
+
+### Ranger Admin (`RangerClient`)
+
+Base URL: `http(s)://<ranger-admin-host>:<port>`
+
+`RangerClient` wraps APIs under `service/public/v2/api`, including:
+
+- service-def CRUD
+- service CRUD
+- policy CRUD/apply/search
+- roles
+- security-zones
+- plugin-info and policy-delta maintenance
+
+Authentication options:
+
+- Basic auth tuple, for example `("admin", "password")`
+- Kerberos/SPNEGO (`requests-kerberos`)
+- custom headers/query params via constructor
+
+Example:
+
+```python
+from apache_ranger.client.ranger_client import RangerClient
+
+ranger = RangerClient("http://localhost:6080", ("admin", "rangerR0cks!"))
+services = ranger.find_services()
+print(len(services.list))
+```
+
+### Ranger User Management (`RangerUserMgmtClient`)
+
+`RangerUserMgmtClient` builds on `RangerClient` and covers user/group/group-user operations, including:
+
+- create/get/update/delete user
+- create/get/update/delete group
+- add/remove/list group-user mappings
+- list groups for user, list users in group
+
+Example:
+
+```python
+from apache_ranger.client.ranger_client import RangerClient
+from apache_ranger.client.ranger_user_mgmt_client import RangerUserMgmtClient
+
+ranger = RangerClient("http://localhost:6080", ("admin", "rangerR0cks!"))
+user_mgmt = RangerUserMgmtClient(ranger)
+
+users = user_mgmt.find_users()
+print(len(users.list))
+```
+
+### Ranger KMS (`RangerKMSClient`)
+
+Base URL: `http(s)://<kms-host>:<port>`
+
+`RangerKMSClient` wraps KMS APIs such as:
+
+- create/delete key
+- rollover/get metadata/current version
+- generate/decrypt/reencrypt encrypted keys
+- status and key-name listing
+
+Authentication options:
+
+- Hadoop simple auth (`HadoopSimpleAuth("user")`)
+- Kerberos/SPNEGO (`requests-kerberos`)
+- Basic auth tuple (when enabled)
+
+Example:
+
+```python
+from apache_ranger.client.ranger_kms_client import RangerKMSClient
+from apache_ranger.client.ranger_client import HadoopSimpleAuth
+
+kms = RangerKMSClient("http://localhost:9292", HadoopSimpleAuth("keyadmin"))
+print(kms.kms_status())
+```
+
+### Ranger PDP (`RangerPDPClient`)
+
+`RangerPDPClient` is a thin Python wrapper for the PDP REST APIs exposed at `http(s)://<pdp-host>:<pdp-port>/authz/v1`.
+
+Endpoints:
+
+- `POST /authz/v1/authorize` -> single access evaluation
+- `POST /authz/v1/authorizeMulti` -> batch access evaluation
+- `POST /authz/v1/permissions` -> effective permissions for a resource
+
+Request context requirements:
+
+- `context.serviceType` (for example: `hive`, `hdfs`, `kafka`)
+- `context.serviceName` (Ranger service name, for example: `dev_hive`)
+- for `authorize` and `authorizeMulti`, `user.name` must be present
+
+Authentication options:
+
+- **Kerberos/SPNEGO**
+ - install dependency: `pip install requests-kerberos`
+ - use `HTTPKerberosAuth()` as `auth` in `RangerPDPClient`
+- **Trusted header**
+ - pass caller header (default `X-Forwarded-User`, configurable by `ranger.pdp.authn.header.username`)
+ - recommended only behind a trusted proxy
+- **JWT bearer**
+ - pass `Authorization: Bearer <token>` in request headers
+
+Delegation behavior:
+
+- if caller differs from `request.user.name`, delegation permission is required
+- delegation users are configured with `ranger.pdp.service.<service-name>.delegation.users`
+ (or wildcard `ranger.pdp.service.*.delegation.users`)
+- without delegation permission, PDP returns `403 FORBIDDEN`
+
+`RangerPDPClient` example (Kerberos):
+
+```python
+from requests_kerberos import HTTPKerberosAuth
+from apache_ranger.client.ranger_pdp_client import RangerPDPClient
+from apache_ranger.model.ranger_authz import (
+ RangerAccessContext, RangerAccessInfo, RangerAuthzRequest,
+ RangerResourceInfo, RangerUserInfo
+)
+
+pdp = RangerPDPClient("http://localhost:6500", HTTPKerberosAuth())
+
+req = RangerAuthzRequest({
+ 'requestId': 'req-1',
+ 'user': RangerUserInfo({'name': 'alice'}),
+ 'access': RangerAccessInfo({
+ 'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email']}),
+ 'action': 'QUERY',
+ 'permissions': ['select']
+ }),
+ 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.authorize(req)
+print(res.decision)
+```
+
+Raw REST example using `requests` (JWT bearer):
+
+```python
+import requests
+
+url = "http://localhost:6500/authz/v1/authorize"
+headers = {
+ "Authorization": "Bearer <jwt-token>",
+ "Content-Type": "application/json"
+}
+payload = {
+ "requestId": "req-1",
+ "user": {"name": "alice"},
+ "access": {
+ "resource": {"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"]},
+ "action": "QUERY",
+ "permissions": ["select"]
+ },
+ "context": {"serviceType": "hive", "serviceName": "dev_hive"}
+}
+
+resp = requests.post(url, headers=headers, json=payload, timeout=30)
+resp.raise_for_status()
+print(resp.json())
+```
+
## Usage
```python test_ranger.py```
@@ -122,113 +289,6 @@ print(' deleted service: id=' + str(created_service.id))
```
-```python test_ranger_kms.py```
-```python
-# test_ranger_kms.py
-from apache_ranger.client.ranger_kms_client import RangerKMSClient
-from apache_ranger.client.ranger_client import HadoopSimpleAuth
-from apache_ranger.model.ranger_kms import RangerKey
-import time
-
-
-##
-## Step 1: create a client to connect to Apache Ranger KMS
-##
-kms_url = 'http://localhost:9292'
-kms_auth = HadoopSimpleAuth('keyadmin')
-
-# For Kerberos authentication
-#
-# from requests_kerberos import HTTPKerberosAuth
-#
-# kms_auth = HTTPKerberosAuth()
-#
-# For HTTP Basic authentication
-#
-# kms_auth = ('keyadmin', 'rangerR0cks!')
-
-kms_client = RangerKMSClient(kms_url, kms_auth)
-
-
-
-##
-## Step 2: Let's call KMS APIs
-##
-
-kms_status = kms_client.kms_status()
-print('kms_status():', kms_status)
-print()
-
-key_name = 'test_' + str(int(time.time() * 1000))
-
-key = kms_client.create_key(RangerKey({'name':key_name}))
-print('create_key(' + key_name + '):', key)
-print()
-
-rollover_key = kms_client.rollover_key(key_name, key.material)
-print('rollover_key(' + key_name + '):', rollover_key)
-print()
-
-kms_client.invalidate_cache_for_key(key_name)
-print('invalidate_cache_for_key(' + key_name + ')')
-print()
-
-key_metadata = kms_client.get_key_metadata(key_name)
-print('get_key_metadata(' + key_name + '):', key_metadata)
-print()
-
-current_key = kms_client.get_current_key(key_name)
-print('get_current_key(' + key_name + '):', current_key)
-print()
-
-encrypted_keys = kms_client.generate_encrypted_key(key_name, 6)
-print('generate_encrypted_key(' + key_name + ', ' + str(6) + '):')
-for i in range(len(encrypted_keys)):
- encrypted_key = encrypted_keys[i]
- decrypted_key = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
- reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
- print(' encrypted_keys[' + str(i) + ']: ', encrypted_key)
- print(' decrypted_key[' + str(i) + ']: ', decrypted_key)
- print(' reencrypted_key[' + str(i) + ']:', reencrypted_key)
-print()
-
-reencrypted_keys = kms_client.batch_reencrypt_encrypted_keys(key_name, encrypted_keys)
-print('batch_reencrypt_encrypted_keys(' + key_name + ', ' + str(len(encrypted_keys)) + '):')
-for i in range(len(reencrypted_keys)):
- print(' batch_reencrypt_encrypted_key[' + str(i) + ']:', reencrypted_keys[i])
-print()
-
-key_versions = kms_client.get_key_versions(key_name)
-print('get_key_versions(' + key_name + '):', len(key_versions))
-for i in range(len(key_versions)):
- print(' key_versions[' + str(i) + ']:', key_versions[i])
-print()
-
-for i in range(len(key_versions)):
- key_version = kms_client.get_key_version(key_versions[i].versionName)
- print('get_key_version(' + str(i) + '):', key_version)
-print()
-
-key_names = kms_client.get_key_names()
-print('get_key_names():', len(key_names))
-for i in range(len(key_names)):
- print(' key_name[' + str(i) + ']:', key_names[i])
-print()
-
-keys_metadata = kms_client.get_keys_metadata(key_names)
-print('get_keys_metadata(' + str(key_names) + '):', len(keys_metadata))
-for i in range(len(keys_metadata)):
- print(' key_metadata[' + str(i) + ']:', keys_metadata[i])
-print()
-
-key = kms_client.get_key(key_name)
-print('get_key(' + key_name + '):', key)
-print()
-
-kms_client.delete_key(key_name)
-print('delete_key(' + key_name + ')')
-```
-
```python test_ranger_user_mgmt.py```
```python
# test_ranger_user_mgmt.py
@@ -365,4 +425,198 @@ print(f'\nDeleting user {user.name}')
user_mgmt.delete_user_by_id(created_user.id, True)
```
-For more examples, checkout `sample-client` python project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python) module.
+```python test_ranger_kms.py```
+```python
+# test_ranger_kms.py
+from apache_ranger.client.ranger_kms_client import RangerKMSClient
+from apache_ranger.client.ranger_client import HadoopSimpleAuth
+from apache_ranger.model.ranger_kms import RangerKey
+import time
+
+
+##
+## Step 1: create a client to connect to Apache Ranger KMS
+##
+kms_url = 'http://localhost:9292'
+kms_auth = HadoopSimpleAuth('keyadmin')
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# kms_auth = HTTPKerberosAuth()
+#
+# For HTTP Basic authentication
+#
+# kms_auth = ('keyadmin', 'rangerR0cks!')
+
+kms_client = RangerKMSClient(kms_url, kms_auth)
+
+
+
+##
+## Step 2: Let's call KMS APIs
+##
+
+kms_status = kms_client.kms_status()
+print('kms_status():', kms_status)
+print()
+
+key_name = 'test_' + str(int(time.time() * 1000))
+
+key = kms_client.create_key(RangerKey({'name':key_name}))
+print('create_key(' + key_name + '):', key)
+print()
+
+rollover_key = kms_client.rollover_key(key_name, key.material)
+print('rollover_key(' + key_name + '):', rollover_key)
+print()
+
+kms_client.invalidate_cache_for_key(key_name)
+print('invalidate_cache_for_key(' + key_name + ')')
+print()
+
+key_metadata = kms_client.get_key_metadata(key_name)
+print('get_key_metadata(' + key_name + '):', key_metadata)
+print()
+
+current_key = kms_client.get_current_key(key_name)
+print('get_current_key(' + key_name + '):', current_key)
+print()
+
+encrypted_keys = kms_client.generate_encrypted_key(key_name, 6)
+print('generate_encrypted_key(' + key_name + ', ' + str(6) + '):')
+for i in range(len(encrypted_keys)):
+ encrypted_key = encrypted_keys[i]
+ decrypted_key = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
+ reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
+ print(' encrypted_keys[' + str(i) + ']: ', encrypted_key)
+ print(' decrypted_key[' + str(i) + ']: ', decrypted_key)
+ print(' reencrypted_key[' + str(i) + ']:', reencrypted_key)
+print()
+
+reencrypted_keys = kms_client.batch_reencrypt_encrypted_keys(key_name, encrypted_keys)
+print('batch_reencrypt_encrypted_keys(' + key_name + ', ' + str(len(encrypted_keys)) + '):')
+for i in range(len(reencrypted_keys)):
+ print(' batch_reencrypt_encrypted_key[' + str(i) + ']:', reencrypted_keys[i])
+print()
+
+key_versions = kms_client.get_key_versions(key_name)
+print('get_key_versions(' + key_name + '):', len(key_versions))
+for i in range(len(key_versions)):
+ print(' key_versions[' + str(i) + ']:', key_versions[i])
+print()
+
+for i in range(len(key_versions)):
+ key_version = kms_client.get_key_version(key_versions[i].versionName)
+ print('get_key_version(' + str(i) + '):', key_version)
+print()
+
+key_names = kms_client.get_key_names()
+print('get_key_names():', len(key_names))
+for i in range(len(key_names)):
+ print(' key_name[' + str(i) + ']:', key_names[i])
+print()
+
+keys_metadata = kms_client.get_keys_metadata(key_names)
+print('get_keys_metadata(' + str(key_names) + '):', len(keys_metadata))
+for i in range(len(keys_metadata)):
+ print(' key_metadata[' + str(i) + ']:', keys_metadata[i])
+print()
+
+key = kms_client.get_key(key_name)
+print('get_key(' + key_name + '):', key)
+print()
+
+kms_client.delete_key(key_name)
+print('delete_key(' + key_name + ')')
+```
+
+```python test_ranger_pdp.py```
+```python
+from apache_ranger.client.ranger_pdp_client import RangerPDPClient
+from apache_ranger.model.ranger_authz import RangerAccessContext, RangerAccessInfo
+from apache_ranger.model.ranger_authz import RangerAuthzRequest, RangerMultiAuthzRequest
+from apache_ranger.model.ranger_authz import RangerResourceInfo, RangerResourcePermissionsRequest, RangerUserInfo
+
+##
+## Step 1: create a client to connect to Ranger PDP
+##
+pdp_url = 'http://localhost:6500'
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# pdp = RangerPDPClient(pdp_url, HTTPKerberosAuth())
+
+# For trusted-header authN with PDP (example only):
+#
+pdp = RangerPDPClient(pdp_url, auth=None, headers={ 'X-Forwarded-User': 'hive' })
+
+##
+## Step 2: call PDP authorization APIs
+##
+req = RangerAuthzRequest({
+ 'requestId': 'req-1',
+ 'user': RangerUserInfo({'name': 'alice'}),
+ 'access': RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}), 'permissions': ['create']}),
+ 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.authorize(req)
+
+print('authorize():')
+print(f' {req}')
+print(f' {res}')
+print('\n')
+
+
+req = RangerAuthzRequest({
+ 'requestId': 'req-2',
+ 'user': RangerUserInfo({'name': 'alice'}),
+ 'access': RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email']}), 'permissions': ['select']}),
+ 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.authorize(req)
+
+print('authorize():')
+print(f' {req}')
+print(f' {res}')
+print('\n')
+
+
+req = RangerMultiAuthzRequest({
+ 'requestId': 'req-3',
+ 'user': RangerUserInfo({'name': 'alice'}),
+ 'accesses': [
+ RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email'], 'attributes': {'OWNER': 'alice'}}), 'permissions': ['select']}),
+ RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_vw1'}), 'permissions': ['create']})
+ ],
+ 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.authorize_multi(req)
+
+print('authorize_multi():')
+print(f' {req}')
+print(f' {res}')
+print('\n')
+
+
+req = RangerResourcePermissionsRequest({
+ 'requestId': 'req-4',
+ 'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}),
+ 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'})
+})
+
+res = pdp.get_resource_permissions(req)
+
+print('get_resource_permissions():')
+print(f' {req}')
+print(f' {res}')
+print('\n')
+```
+
+For more examples, checkout `sample-client` python project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python) module (including `sample_client.py`, `user_mgmt.py`, `sample_kms_client.py`, and `sample_pdp_client.py`).
diff --git a/intg/src/main/python/apache_ranger/client/__init__.py b/intg/src/main/python/apache_ranger/client/__init__.py
index ed9d0b3c4..e4f911e7e 100644
--- a/intg/src/main/python/apache_ranger/client/__init__.py
+++ b/intg/src/main/python/apache_ranger/client/__init__.py
@@ -15,3 +15,34 @@
# 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.
+
+from apache_ranger.client.ranger_client import RangerClient, RangerClientPrivate, HadoopSimpleAuth
+from apache_ranger.client.ranger_gds_client import RangerGdsClient
+from apache_ranger.client.ranger_kms_client import RangerKMSClient
+from apache_ranger.client.ranger_pdp_client import RangerPDPClient
+from apache_ranger.client.ranger_user_mgmt_client import RangerUserMgmtClient
+from apache_ranger.model.ranger_authz import RangerAccessContext, RangerAccessInfo, RangerAuthzRequest, RangerAuthzResult
+from apache_ranger.model.ranger_authz import RangerMultiAuthzRequest, RangerMultiAuthzResult
+from apache_ranger.model.ranger_authz import RangerPermissionResult, RangerResourceInfo
+from apache_ranger.model.ranger_authz import RangerResourcePermissions, RangerResourcePermissionsRequest, RangerUserInfo
+
+__all__ = [
+ "RangerClient",
+ "RangerClientPrivate",
+ "RangerGdsClient",
+ "RangerKMSClient",
+ "RangerPDPClient",
+ "RangerUserMgmtClient",
+ "RangerAccessContext",
+ "RangerAccessInfo",
+ "RangerAuthzRequest",
+ "RangerAuthzResult",
+ "RangerMultiAuthzRequest",
+ "RangerMultiAuthzResult",
+ "RangerPermissionResult",
+ "RangerResourceInfo",
+ "RangerResourcePermissions",
+ "RangerResourcePermissionsRequest",
+ "RangerUserInfo",
+ "HadoopSimpleAuth",
+]
diff --git a/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py b/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py
new file mode 100644
index 000000000..9268a2f4c
--- /dev/null
+++ b/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+import logging
+from apache_ranger.client.ranger_client import RangerClientHttp
+from apache_ranger.model.ranger_authz import RangerAuthzRequest, RangerAuthzResult
+from apache_ranger.model.ranger_authz import RangerMultiAuthzRequest, RangerMultiAuthzResult
+from apache_ranger.model.ranger_authz import RangerResourcePermissions, RangerResourcePermissionsRequest
+from apache_ranger.utils import API, HttpMethod, HTTPStatus
+from apache_ranger.utils import type_coerce
+
+LOG = logging.getLogger(__name__)
+
+
+class RangerPDPClient:
+ """
+ Python client for Ranger PDP authorization APIs.
+
+ Base URL should point to PDP server endpoint, for example:
+ http://localhost:6500
+ """
+
+ def __init__(self, url, auth, query_params=None, headers=None):
+ self.client_http = RangerClientHttp(url, auth, query_params, headers)
+ self.session = self.client_http.session
+ logging.getLogger("requests").setLevel(logging.WARNING)
+
+ def authorize(self, authz_request):
+ """
+ Call POST /authz/v1/authorize
+ :param authz_request: dict-like or RangerAuthzRequest
+ :return: RangerAuthzResult
+ """
+ req = type_coerce(authz_request, RangerAuthzRequest)
+ resp = self.client_http.call_api(RangerPDPClient.AUTHORIZE, request_data=req)
+ return type_coerce(resp, RangerAuthzResult)
+
+ def authorize_multi(self, multi_authz_request):
+ """
+ Call POST /authz/v1/authorizeMulti
+ :param multi_authz_request: dict-like or RangerMultiAuthzRequest
+ :return: RangerMultiAuthzResult
+ """
+ req = type_coerce(multi_authz_request, RangerMultiAuthzRequest)
+ resp = self.client_http.call_api(RangerPDPClient.AUTHORIZE_MULTI, request_data=req)
+ return type_coerce(resp, RangerMultiAuthzResult)
+
+ def get_resource_permissions(self, resource_perm_request):
+ """
+ Call POST /authz/v1/permissions
+ :param resource_perm_request: dict-like OR RangerResourcePermissionsRequest
+ :return: RangerResourcePermissions
+ """
+ req = type_coerce(resource_perm_request, RangerResourcePermissionsRequest)
+ resp = self.client_http.call_api(RangerPDPClient.GET_RESOURCE_PERMISSIONS, request_data=req)
+
+ return type_coerce(resp, RangerResourcePermissions)
+
+ # URIs
+ URI_BASE = "authz/v1"
+ URI_AUTHORIZE = URI_BASE + "/authorize"
+ URI_AUTHORIZE_MULTI = URI_BASE + "/authorizeMulti"
+ URI_RESOURCE_PERMISSIONS = URI_BASE + "/permissions"
+
+ # APIs
+ AUTHORIZE = API(URI_AUTHORIZE, HttpMethod.POST, HTTPStatus.OK)
+ AUTHORIZE_MULTI = API(URI_AUTHORIZE_MULTI, HttpMethod.POST, HTTPStatus.OK)
+ GET_RESOURCE_PERMISSIONS = API(URI_RESOURCE_PERMISSIONS, HttpMethod.POST, HTTPStatus.OK)
+
diff --git a/intg/src/main/python/apache_ranger/exceptions.py b/intg/src/main/python/apache_ranger/exceptions.py
index 9f19a59ac..347468a83 100644
--- a/intg/src/main/python/apache_ranger/exceptions.py
+++ b/intg/src/main/python/apache_ranger/exceptions.py
@@ -33,17 +33,21 @@ def __init__(self, api, response):
self.msgDesc = None
self.messageList = None
- print(response)
-
if api is not None and response is not None:
if response.content:
- try:
- respJson = response.json()
- self.msgDesc = respJson['msgDesc'] if respJson is not None and 'msgDesc' in respJson else None
- self.messageList = respJson['messageList'] if respJson is not None and 'messageList' in respJson else None
- except Exception:
- self.msgDesc = response.content
- self.messageList = [ response.content ]
+ try:
+ respJson = response.json()
+
+ if respJson:
+ self.msgDesc = respJson['msgDesc'] if 'msgDesc' in respJson else None
+ self.messageList = respJson['messageList'] if 'messageList' in respJson else None
+
+ if self.msgDesc is None:
+ self.msgDesc = respJson['message'] if 'message' in respJson else None
+
+ except Exception:
+ self.msgDesc = response.content
+ self.messageList = [ response.content ]
self.statusCode = response.status_code
diff --git a/intg/src/main/python/apache_ranger/model/__init__.py b/intg/src/main/python/apache_ranger/model/__init__.py
index ed9d0b3c4..ca8a51a7e 100644
--- a/intg/src/main/python/apache_ranger/model/__init__.py
+++ b/intg/src/main/python/apache_ranger/model/__init__.py
@@ -15,3 +15,22 @@
# 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.
+
+from apache_ranger.model.ranger_authz import RangerAccessContext, RangerAccessInfo, RangerAuthzRequest, RangerAuthzResult
+from apache_ranger.model.ranger_authz import RangerMultiAuthzRequest, RangerMultiAuthzResult
+from apache_ranger.model.ranger_authz import RangerPermissionResult, RangerResourceInfo
+from apache_ranger.model.ranger_authz import RangerResourcePermissions, RangerResourcePermissionsRequest, RangerUserInfo
+
+__all__ = [
+ "RangerAccessContext",
+ "RangerAccessInfo",
+ "RangerAuthzRequest",
+ "RangerAuthzResult",
+ "RangerMultiAuthzRequest",
+ "RangerMultiAuthzResult",
+ "RangerPermissionResult",
+ "RangerResourceInfo",
+ "RangerResourcePermissions",
+ "RangerResourcePermissionsRequest",
+ "RangerUserInfo",
+]
diff --git a/intg/src/main/python/apache_ranger/model/ranger_authz.py b/intg/src/main/python/apache_ranger/model/ranger_authz.py
new file mode 100644
index 000000000..ca4b90613
--- /dev/null
+++ b/intg/src/main/python/apache_ranger/model/ranger_authz.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+from apache_ranger.model.ranger_base import RangerBase
+from apache_ranger.utils import non_null, type_coerce, type_coerce_dict, type_coerce_list
+
+
+class RangerUserInfo(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+
+ self.name = attrs.get("name")
+ self.attributes = attrs.get("attributes")
+ self.groups = attrs.get("groups")
+ self.roles = attrs.get("roles")
+
+
+class RangerResourceInfo(RangerBase):
+ SCOPE_SELF = "SELF"
+ SCOPE_SELF_OR_ANY_CHILD = "SELF_OR_ANY_CHILD"
+ SCOPE_SELF_OR_ANY_DESCENDANT = "SELF_OR_ANY_DESCENDANT"
+
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+
+ self.name = attrs.get("name")
+ self.subResources = attrs.get("subResources")
+ self.nameMatchScope = attrs.get("nameMatchScope")
+ self.attributes = attrs.get("attributes")
+
+
+class RangerAccessInfo(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+
+ self.resource = attrs.get("resource")
+ self.action = attrs.get("action")
+ self.permissions = attrs.get("permissions")
+
+ def type_coerce_attrs(self):
+ super(RangerAccessInfo, self).type_coerce_attrs()
+ self.resource = type_coerce(self.resource, RangerResourceInfo)
+
+
+class RangerAccessContext(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+
+ self.serviceType = attrs.get("serviceType")
+ self.serviceName = attrs.get("serviceName")
+ self.accessTime = attrs.get("accessTime")
+ self.clientIpAddress = attrs.get("clientIpAddress")
+ self.forwardedIpAddresses = attrs.get("forwardedIpAddresses")
+ self.additionalInfo = attrs.get("additionalInfo")
+
+
+class RangerAuthzRequest(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+
+ self.requestId = attrs.get("requestId")
+ self.user = attrs.get("user")
+ self.access = attrs.get("access")
+ self.context = attrs.get("context")
+
+ def type_coerce_attrs(self):
+ super(RangerAuthzRequest, self).type_coerce_attrs()
+ self.user = type_coerce(self.user, RangerUserInfo)
+ self.access = type_coerce(self.access, RangerAccessInfo)
+ self.context = type_coerce(self.context, RangerAccessContext)
+
+
+class RangerMultiAuthzRequest(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+
+ self.requestId = attrs.get("requestId")
+ self.user = attrs.get("user")
+ self.accesses = attrs.get("accesses")
+ self.context = attrs.get("context")
+
+ def type_coerce_attrs(self):
+ super(RangerMultiAuthzRequest, self).type_coerce_attrs()
+ self.user = type_coerce(self.user, RangerUserInfo)
+ self.accesses = type_coerce_list(self.accesses, RangerAccessInfo)
+ self.context = type_coerce(self.context, RangerAccessContext)
+
+
+class RangerPolicyInfo(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.id = attrs.get("id")
+ self.version = attrs.get("version")
+
+
+class RangerAccessResult(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.decision = attrs.get("decision")
+ self.policy = attrs.get("policy")
+
+ def type_coerce_attrs(self):
+ super(RangerAccessResult, self).type_coerce_attrs()
+ self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerDataMaskResult(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.maskType = attrs.get("maskType")
+ self.maskedValue = attrs.get("maskedValue")
+ self.policy = attrs.get("policy")
+
+ def type_coerce_attrs(self):
+ super(RangerDataMaskResult, self).type_coerce_attrs()
+ self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerRowFilterResult(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.filterExpr = attrs.get("filterExpr")
+ self.policy = attrs.get("policy")
+
+ def type_coerce_attrs(self):
+ super(RangerRowFilterResult, self).type_coerce_attrs()
+ self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerResultInfo(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.access = attrs.get("access")
+ self.dataMask = attrs.get("dataMask")
+ self.rowFilter = attrs.get("rowFilter")
+ self.additionalInfo = attrs.get("additionalInfo")
+
+ def type_coerce_attrs(self):
+ super(RangerResultInfo, self).type_coerce_attrs()
+ self.access = type_coerce(self.access, RangerAccessResult)
+ self.dataMask = type_coerce(self.dataMask, RangerDataMaskResult)
+ self.rowFilter = type_coerce(self.rowFilter, RangerRowFilterResult)
+
+
+class RangerPermissionResult(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.permission = attrs.get("permission")
+ self.access = attrs.get("access")
+ self.dataMask = attrs.get("dataMask")
+ self.rowFilter = attrs.get("rowFilter")
+ self.additionalInfo = attrs.get("additionalInfo")
+ self.subResources = attrs.get("subResources")
+
+ def type_coerce_attrs(self):
+ super(RangerPermissionResult, self).type_coerce_attrs()
+ self.access = type_coerce(self.access, RangerAccessResult)
+ self.dataMask = type_coerce(self.dataMask, RangerDataMaskResult)
+ self.rowFilter = type_coerce(self.rowFilter, RangerRowFilterResult)
+ self.subResources = type_coerce_dict(self.subResources, RangerResultInfo)
+
+
+class RangerAuthzResult(RangerBase):
+ DECISION_ALLOW = "ALLOW"
+ DECISION_DENY = "DENY"
+ DECISION_NOT_DETERMINED = "NOT_DETERMINED"
+ DECISION_PARTIAL = "PARTIAL"
+
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.requestId = attrs.get("requestId")
+ self.decision = attrs.get("decision")
+ self.permissions = attrs.get("permissions")
+
+ def type_coerce_attrs(self):
+ super(RangerAuthzResult, self).type_coerce_attrs()
+ self.permissions = type_coerce_dict(self.permissions, RangerPermissionResult)
+
+
+class RangerMultiAuthzResult(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.requestId = attrs.get("requestId")
+ self.decision = attrs.get("decision")
+ self.accesses = attrs.get("accesses")
+
+ def type_coerce_attrs(self):
+ super(RangerMultiAuthzResult, self).type_coerce_attrs()
+ self.accesses = type_coerce_list(self.accesses, RangerAuthzResult)
+
+
+class RangerResourcePermissionsRequest(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.requestId = attrs.get("requestId")
+ self.resource = attrs.get("resource")
+ self.context = attrs.get("context")
+
+ def type_coerce_attrs(self):
+ super(RangerResourcePermissionsRequest, self).type_coerce_attrs()
+ self.resource = type_coerce(self.resource, RangerResourceInfo)
+ self.context = type_coerce(self.context, RangerAccessContext)
+
+
+class RangerResourcePermissions(RangerBase):
+ def __init__(self, attrs=None):
+ attrs = non_null(attrs, {})
+ RangerBase.__init__(self, attrs)
+ self.resource = attrs.get("resource")
+ self.users = attrs.get("users")
+ self.groups = attrs.get("groups")
+ self.roles = attrs.get("roles")
+
+ def type_coerce_attrs(self):
+ super(RangerResourcePermissions, self).type_coerce_attrs()
+ self.resource = type_coerce(self.resource, RangerResourceInfo)
+ self.users = _coerce_principal_permissions(self.users)
+ self.groups = _coerce_principal_permissions(self.groups)
+ self.roles = _coerce_principal_permissions(self.roles)
+
+
+def _coerce_principal_permissions(value):
+ if not isinstance(value, dict):
+ return None
+
+ ret = {}
+ for principal, permissions in value.items():
+ ret[principal] = type_coerce_dict(permissions, RangerPermissionResult)
+ return ret
+
diff --git a/pdp/conf.dist/README-k8s.md b/pdp/conf.dist/README-k8s.md
new file mode 100644
index 000000000..b630bec7d
--- /dev/null
+++ b/pdp/conf.dist/README-k8s.md
@@ -0,0 +1,103 @@
+<!---
+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.
+-->
+# Ranger PDP Kubernetes Notes
+
+This document captures Kubernetes-oriented runtime recommendations for `ranger-pdp`.
+
+## Health Probes
+
+Use:
+
+- Liveness: `GET /health/live`
+- Readiness: `GET /health/ready`
+
+The readiness endpoint reports NOT_READY until:
+
+- server is started
+- authorizer is initialized
+- PDP is accepting requests
+
+## Metrics
+
+PDP exposes Prometheus-style text metrics at:
+
+- `GET /metrics`
+
+Current metrics include request counts, auth failures, average latency and loaded services count.
+
+## Runtime Tuning
+
+Thread/connection controls can be set in `ranger-pdp-site.xml` or via `JAVA_OPTS -D...`:
+
+- `ranger.pdp.http.connector.maxThreads`
+- `ranger.pdp.http.connector.minSpareThreads`
+- `ranger.pdp.http.connector.acceptCount`
+- `ranger.pdp.http.connector.maxConnections`
+
+## Logging
+
+For container-native logging, logback can emit to stdout.
+If file logging is needed, pass:
+
+`-Dlogdir=/path/to/log/dir`
+
+and use a file appender that resolves `${logdir}`.
+
+## Security Context
+
+Recommended:
+
+- run as non-root user
+- readOnlyRootFilesystem: true
+- mount writable volumes only for required paths (cache/log/temp if file logging is enabled)
+
+## Config/Secrets
+
+Recommended:
+
+- `ranger-pdp-site.xml` from ConfigMap
+- keytabs/JWT keys/credentials from Secrets
+
+## Authentication Config Keys
+
+When configuring inbound PDP authentication in `ranger-pdp-site.xml`, use the
+`ranger.pdp.authn.*` property names:
+
+- `ranger.pdp.authn.types`
+- `ranger.pdp.authn.header.enabled`
+- `ranger.pdp.authn.header.username`
+- `ranger.pdp.authn.jwt.enabled`
+- `ranger.pdp.authn.jwt.provider.url`
+- `ranger.pdp.authn.jwt.public.key`
+- `ranger.pdp.authn.jwt.cookie.name`
+- `ranger.pdp.authn.jwt.audiences`
+- `ranger.pdp.authn.kerberos.enabled`
+- `ranger.pdp.authn.kerberos.spnego.principal`
+- `ranger.pdp.authn.kerberos.spnego.keytab`
+- `ranger.pdp.authn.kerberos.token.valid.seconds`
+- `ranger.pdp.authn.kerberos.name.rules`
+
+## Network Policy
+
+Allow egress only to dependencies:
+
+- Ranger Admin
+- Solr (if audit destination enabled)
+- KDC (if Kerberos is used)
+
diff --git a/pdp/conf.dist/logback.xml b/pdp/conf.dist/logback.xml
new file mode 100644
index 000000000..a52ffde14
--- /dev/null
+++ b/pdp/conf.dist/logback.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<configuration>
+ <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
+ <target>System.out</target>
+ <encoder>
+ <pattern>%d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n</pattern>
+ </encoder>
+ </appender>
+
+ <logger name="com.sun.jersey.server.wadl.generators.WadlGeneratorJAXBGrammarGenerator" level="OFF"/>
+ <logger name="org.apache.directory.server.core" level="OFF"/>
+ <logger name="org.apache.hadoop.security" level="OFF"/>
+ <logger name="org.apache.hadoop.conf" level="ERROR"/>
+ <logger name="org.apache.hadoop.util.NativeCodeLoader" level="OFF"/>
+
+ <root level="INFO">
+ <appender-ref ref="stdout"/>
+ </root>
+</configuration>
diff --git a/pdp/conf.dist/ranger-pdp-site.xml b/pdp/conf.dist/ranger-pdp-site.xml
new file mode 100644
index 000000000..ddf2ff934
--- /dev/null
+++ b/pdp/conf.dist/ranger-pdp-site.xml
@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+ 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.
+-->
+<configuration>
+ <property>
+ <name>ranger.pdp.port</name>
+ <value>6500</value>
+ <description>Port the PDP server listens on.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.log.dir</name>
+ <value>/var/log/ranger/pdp</value>
+ <description>Directory for PDP server log files.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.service.*.delegation.users</name>
+ <value/>
+ <description>Comma-separated users, allowed to call on behalf of other users in all services</description>
+ </property>
+
+ <!-- SSL/TLS -->
+ <property>
+ <name>ranger.pdp.ssl.enabled</name>
+ <value>false</value>
+ <description>Set to true to enable HTTPS.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.keystore.file</name>
+ <value/>
+ <description>Path to the keystore file (required when SSL is enabled).</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.keystore.password</name>
+ <value/>
+ <description>Keystore password.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.keystore.type</name>
+ <value>JKS</value>
+ <description>Keystore type (JKS, PKCS12).</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.truststore.enabled</name>
+ <value>false</value>
+ <description>Set to true to require client certificate authentication.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.truststore.file</name>
+ <value/>
+ <description>Path to the truststore file.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.truststore.password</name>
+ <value/>
+ <description>Truststore password.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.truststore.type</name>
+ <value>JKS</value>
+ <description>Truststore type (JKS, PKCS12).</description>
+ </property>
+
+ <!-- HTTP/2 -->
+ <property>
+ <name>ranger.pdp.http2.enabled</name>
+ <value>true</value>
+ <description>
+ Enable HTTP/2 via upgrade on the connector.
+ Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside HTTP/1.1.
+ </description>
+ </property>
+
+ <!-- Connector concurrency / queue limits -->
+ <property>
+ <name>ranger.pdp.http.connector.maxThreads</name>
+ <value>200</value>
+ <description>Maximum number of worker threads handling simultaneous requests.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.minSpareThreads</name>
+ <value>20</value>
+ <description>Minimum number of spare worker threads kept ready.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.acceptCount</name>
+ <value>100</value>
+ <description>Queued connection backlog when all worker threads are busy.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.maxConnections</name>
+ <value>10000</value>
+ <description>Maximum concurrent TCP connections accepted by the connector.</description>
+ </property>
+
+ <!-- Authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.types</name>
+ <value>header,jwt,kerberos</value>
+ <description>
+ Comma-separated list of authentication methods for incoming REST requests,
+ tried in listed order. Supported values: header, jwt, kerberos.
+ </description>
+ </property>
+
+ <!-- HTTP Header authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.header.enabled</name>
+ <value>false</value>
+ <description>
+ Enable trusted HTTP header authentication. Use only behind a trusted proxy.
+ </description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.header.username</name>
+ <value>X-Forwarded-User</value>
+ <description>HTTP header name from which the authenticated username is read.</description>
+ </property>
+
+ <!-- JWT bearer token authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.jwt.enabled</name>
+ <value>false</value>
+ <description>Enable JWT authentication.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.provider.url</name>
+ <value/>
+ <description>URL of the JWT provider (used to fetch the public key).</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.public.key</name>
+ <value/>
+ <description>PEM-encoded public key for verifying JWT signatures.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.cookie.name</name>
+ <value>hadoop-jwt</value>
+ <description>Cookie name from which a JWT bearer token may be read.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.audiences</name>
+ <value/>
+ <description>Comma-separated list of accepted JWT audiences. Empty means any audience is accepted.</description>
+ </property>
+
+ <!-- Kerberos / SPNEGO authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.kerberos.enabled</name>
+ <value>false</value>
+ <description>Enable Kerberos authentication.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.spnego.principal</name>
+ <value/>
+ <description>Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.spnego.keytab</name>
+ <value/>
+ <description>Path to the keytab file for the SPNEGO service principal.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.token.valid.seconds</name>
+ <value>3600</value>
+ <description>Validity period (seconds) for Kerberos authentication tokens.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.name.rules</name>
+ <value>DEFAULT</value>
+ <description>Rules for mapping Kerberos principal names to short usernames.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.app.type</name>
+ <value>ranger-pdp</value>
+ <description>Application type reported in audit records.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.init.services</name>
+ <value/>
+ <description>
+ Comma-separated list of Ranger service names to eagerly initialize at startup.
+ Leave empty for lazy initialization on first access.
+ Example: dev_hive,dev_hdfs
+ </description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.source.impl</name>
+ <value>org.apache.ranger.admin.client.RangerAdminRESTClient</value>
+ <description>Policy source implementation. Use RangerAdminRESTClient to download policies live from Ranger Admin.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.url</name>
+ <value>http://localhost:6080</value>
+ <description>
+ URL of the Ranger Admin server. Comma-separated for HA.
+ Example: http://ranger-admin-1:6080,http://ranger-admin-2:6080
+ </description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.ssl.config.file</name>
+ <value/>
+ <description>
+ Path to the XML file containing SSL keystore/truststore settings for the
+ connection to Ranger Admin (required when the Admin URL uses https://).
+ The file uses the standard Ranger policymgr SSL property names:
+ xasecure.policymgr.clientssl.truststore
+ xasecure.policymgr.clientssl.truststore.credential.file
+ xasecure.policymgr.clientssl.keystore (for mutual TLS)
+ xasecure.policymgr.clientssl.keystore.credential.file
+ </description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.client.connection.timeoutMs</name>
+ <value>120000</value>
+ <description>HTTP connection timeout (ms) for calls to Ranger Admin.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.client.read.timeoutMs</name>
+ <value>30000</value>
+ <description>HTTP read timeout (ms) for calls to Ranger Admin.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.pollIntervalMs</name>
+ <value>30000</value>
+ <description>How often (ms) to poll Ranger Admin for updated policies and roles.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.cache.dir</name>
+ <value>/var/ranger/cache/pdp</value>
+ <description>
+ Directory for local JSON cache files (policies, tags, roles, userstore, GDS).
+ The PDP can serve authorization requests from cache while Ranger Admin is unavailable.
+ </description>
+ </property>
+
+ <!-- Basic Auth authentication for connecting to Ranger Admin -->
+ <property>
+ <name>ranger.authz.default.policy.rest.client.username</name>
+ <value>admin</value>
+ <description>Username for connecting to Ranger Admin (basic auth).</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.client.password</name>
+ <value>admin</value>
+ <description>Password for connecting to Ranger Admin (basic auth).</description>
+ </property>
+
+ <!-- Kerberos authentication for connecting to Ranger Admin-->
+ <property>
+ <name>ranger.authz.default.ugi.initialize</name>
+ <value>false</value>
+ <description>Set to true to enable Kerberos login for the Ranger Admin connection.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.ugi.login.type</name>
+ <value/>
+ <description>Kerberos login type: keytab or jaas.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.ugi.keytab.principal</name>
+ <value/>
+ <description>Kerberos principal for keytab-based login.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.ugi.keytab.file</name>
+ <value/>
+ <description>Path to the keytab file for keytab-based login.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.ugi.jaas.appconfig</name>
+ <value/>
+ <description>JAAS application configuration name for jaas-based login.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.use.rangerGroups</name>
+ <value>false</value>
+ <description>
+ When true, group membership is resolved from Ranger's internal userstore
+ (downloaded from Ranger Admin) rather than the local OS.
+ </description>
+ </property>
+
+ <!-- Audit -->
+ <property>
+ <name>ranger.authz.audit.is.enabled</name>
+ <value>true</value>
+ <description>Master switch for audit logging.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.solr</name>
+ <value>false</value>
+ <description>Enable Solr as an audit destination.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.solr.urls</name>
+ <value/>
+ <description>Solr URL for audit logging.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.hdfs</name>
+ <value>false</value>
+ <description>Enable HDFS as an audit destination.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.hdfs.dir</name>
+ <value/>
+ <description>HDFS directory path for audit log files.</description>
+ </property>
+</configuration>
diff --git a/pdp/pom.xml b/pdp/pom.xml
new file mode 100644
index 000000000..48da6dd21
--- /dev/null
+++ b/pdp/pom.xml
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.ranger</groupId>
+ <artifactId>ranger</artifactId>
+ <version>3.0.0-SNAPSHOT</version>
+ <relativePath>..</relativePath>
+ </parent>
+
+ <artifactId>ranger-pdp</artifactId>
+ <description>Ranger Policy Decision Point (PDP) Server</description>
+
+ <properties>
+ <jersey2.version>2.35</jersey2.version>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ <version>${logback.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <version>${fasterxml.jackson.databind.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.module</groupId>
+ <artifactId>jackson-module-jaxb-annotations</artifactId>
+ <version>${fasterxml.jackson.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.nimbusds</groupId>
+ <artifactId>nimbus-jose-jwt</artifactId>
+ <version>${nimbus-jose-jwt.version}</version>
+ </dependency>
+ <!-- Jersey-1 client pieces needed by ranger-plugins-common -->
+ <dependency>
+ <groupId>com.sun.jersey</groupId>
+ <artifactId>jersey-client</artifactId>
+ <version>${jersey-core.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.sun.jersey</groupId>
+ <artifactId>jersey-core</artifactId>
+ <version>${jersey-core.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>${commons.codec.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>jakarta.ws.rs</groupId>
+ <artifactId>jakarta.ws.rs-api</artifactId>
+ <version>2.1.6</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.inject</groupId>
+ <artifactId>javax.inject</artifactId>
+ <version>${javax-inject.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>javax.validation</groupId>
+ <artifactId>validation-api</artifactId>
+ <version>${javax.validation}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>${commons.lang3.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ranger</groupId>
+ <artifactId>authz-embedded</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ranger</groupId>
+ <artifactId>ranger-authn</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ranger</groupId>
+ <artifactId>ranger-plugin-classloader</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ranger</groupId>
+ <artifactId>ranger-plugins-common</artifactId>
+ <version>${project.version}</version>
+ <exclusions>
+ <!--
+ Avoid bringing jersey-bundle (Jersey-1 aggregate JAR) because it
+ conflicts with Jersey-2 server runtime used by PDP.
+ -->
+ <exclusion>
+ <groupId>com.sun.jersey</groupId>
+ <artifactId>jersey-bundle</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ranger</groupId>
+ <artifactId>ugsync-util</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tomcat.embed</groupId>
+ <artifactId>tomcat-embed-core</artifactId>
+ <version>${tomcat.embed.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.tomcat.embed</groupId>
+ <artifactId>tomcat-embed-jasper</artifactId>
+ <version>${tomcat.embed.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.containers</groupId>
+ <artifactId>jersey-container-servlet-core</artifactId>
+ <version>${jersey2.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.core</groupId>
+ <artifactId>jersey-server</artifactId>
+ <version>${jersey2.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.inject</groupId>
+ <artifactId>jersey-hk2</artifactId>
+ <version>${jersey2.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-json-jackson</artifactId>
+ <version>${jersey2.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${slf4j.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <version>${javax.servlet.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-api</artifactId>
+ <version>${junit.jupiter.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter-engine</artifactId>
+ <version>${junit.jupiter.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>org.apache.ranger.pdp.RangerPdpServer</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/pdp/scripts/ranger-pdp-services.sh b/pdp/scripts/ranger-pdp-services.sh
new file mode 100644
index 000000000..d3fba5398
--- /dev/null
+++ b/pdp/scripts/ranger-pdp-services.sh
@@ -0,0 +1,223 @@
+#!/bin/bash
+
+# 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.
+
+if [[ -z $1 ]]; then
+ echo "No argument provided."
+ echo "Usage: $0 {start | run | stop | restart | version}"
+ exit 1
+fi
+
+action=$1
+action=$(echo "$action" | tr '[:lower:]' '[:upper:]')
+
+realScriptPath=$(readlink -f "$0")
+realScriptDir=$(dirname "$realScriptPath")
+cd "$realScriptDir" || exit 1
+cdir=$(pwd)
+
+ranger_pdp_max_heap_size=${RANGER_PDP_MAX_HEAP_SIZE:-1g}
+
+for custom_env_script in $(find "${cdir}/conf/" -name "ranger-pdp-env*" 2>/dev/null); do
+ if [ -f "$custom_env_script" ]; then
+ . "$custom_env_script"
+ fi
+done
+
+if [ -z "${RANGER_PDP_PID_DIR_PATH}" ]; then
+ RANGER_PDP_PID_DIR_PATH=/var/run/ranger
+fi
+
+if [ -z "${RANGER_PDP_PID_NAME}" ]; then
+ RANGER_PDP_PID_NAME=pdp.pid
+fi
+
+pidf="${RANGER_PDP_PID_DIR_PATH}/${RANGER_PDP_PID_NAME}"
+
+if [ -z "${UNIX_PDP_USER}" ]; then
+ UNIX_PDP_USER=ranger
+fi
+
+JAVA_OPTS=" ${JAVA_OPTS} -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=200m -Xmx${ranger_pdp_max_heap_size} -Xms256m"
+
+if [ "${action}" == "START" ]; then
+
+ if [ -f "${cdir}/conf/java_home.sh" ]; then
+ . "${cdir}/conf/java_home.sh"
+ fi
+
+ for custom_env_script in $(find "${cdir}/conf.dist/" -name "ranger-pdp-env*" 2>/dev/null); do
+ if [ -f "$custom_env_script" ]; then
+ . "$custom_env_script"
+ fi
+ done
+
+ if [ "$JAVA_HOME" != "" ]; then
+ export PATH=$JAVA_HOME/bin:$PATH
+ fi
+
+ if [ -z "${RANGER_PDP_LOG_DIR}" ]; then
+ RANGER_PDP_LOG_DIR=/var/log/ranger/pdp
+ fi
+
+ if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then
+ mkdir -p "$RANGER_PDP_LOG_DIR"
+ chmod 755 "$RANGER_PDP_LOG_DIR"
+ fi
+
+ cp="${cdir}/conf:${cdir}/dist/*:${cdir}/lib/*"
+
+ if [ -f "$pidf" ]; then
+ pid=$(cat "$pidf")
+ if ps -p "$pid" > /dev/null 2>&1; then
+ echo "Ranger PDP Service is already running [pid=${pid}]"
+ exit 0
+ else
+ rm -f "$pidf"
+ fi
+ fi
+
+ if [ -z "${RANGER_PDP_CONF_DIR}" ]; then
+ RANGER_PDP_CONF_DIR=${cdir}/conf
+ fi
+
+ mkdir -p "${RANGER_PDP_PID_DIR_PATH}"
+
+ SLEEP_TIME_AFTER_START=5
+ nohup java -Dproc_rangerpdp ${JAVA_OPTS} \
+ -Dlogdir="${RANGER_PDP_LOG_DIR}" \
+ -Dlogback.configurationFile="file:${RANGER_PDP_CONF_DIR}/logback.xml" \
+ -Dranger.pdp.conf.dir="${RANGER_PDP_CONF_DIR}" \
+ -Duser="${USER}" \
+ -Dhostname="${HOSTNAME}" \
+ -cp "${cp}" \
+ org.apache.ranger.pdp.RangerPdpServer \
+ > "${RANGER_PDP_LOG_DIR}/pdp.out" 2>&1 &
+
+ VALUE_OF_PID=$!
+ echo "Starting Ranger PDP Service"
+ sleep $SLEEP_TIME_AFTER_START
+
+ if ps -p $VALUE_OF_PID > /dev/null 2>&1; then
+ echo $VALUE_OF_PID > "${pidf}"
+ chown "${UNIX_PDP_USER}" "${pidf}" 2>/dev/null || true
+ chmod 660 "${pidf}"
+ pid=$(cat "$pidf")
+ echo "Ranger PDP Service with pid ${pid} has started."
+ else
+ echo "Ranger PDP Service failed to start!"
+ exit 1
+ fi
+ exit 0
+
+elif [ "${action}" == "RUN" ]; then
+ if [ -f "${cdir}/conf/java_home.sh" ]; then
+ . "${cdir}/conf/java_home.sh"
+ fi
+
+ for custom_env_script in $(find "${cdir}/conf.dist/" -name "ranger-pdp-env*" 2>/dev/null); do
+ if [ -f "$custom_env_script" ]; then
+ . "$custom_env_script"
+ fi
+ done
+
+ if [ "$JAVA_HOME" != "" ]; then
+ export PATH=$JAVA_HOME/bin:$PATH
+ fi
+
+ if [ -z "${RANGER_PDP_LOG_DIR}" ]; then
+ RANGER_PDP_LOG_DIR=/var/log/ranger/pdp
+ fi
+
+ if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then
+ mkdir -p "$RANGER_PDP_LOG_DIR"
+ chmod 755 "$RANGER_PDP_LOG_DIR"
+ fi
+
+ if [ -z "${RANGER_PDP_CONF_DIR}" ]; then
+ RANGER_PDP_CONF_DIR=${cdir}/conf
+ fi
+
+ cp="${cdir}/conf:${cdir}/dist/*:${cdir}/lib/*"
+ exec java -Dproc_rangerpdp ${JAVA_OPTS} \
+ -Dlogdir="${RANGER_PDP_LOG_DIR}" \
+ -Dlogback.configurationFile="file:${RANGER_PDP_CONF_DIR}/logback.xml" \
+ -Dranger.pdp.conf.dir="${RANGER_PDP_CONF_DIR}" \
+ -Duser="${USER}" \
+ -Dhostname="${HOSTNAME}" \
+ -cp "${cp}" \
+ org.apache.ranger.pdp.RangerPdpServer
+
+elif [ "${action}" == "STOP" ]; then
+
+ WAIT_TIME_FOR_SHUTDOWN=2
+ NR_ITER_FOR_SHUTDOWN_CHECK=15
+
+ if [ -f "$pidf" ]; then
+ pid=$(cat "$pidf")
+ else
+ pid=$(ps -ef | grep java | grep -- '-Dproc_rangerpdp' | grep -v grep | awk '{ print $2 }')
+ if [ "$pid" != "" ]; then
+ echo "pid file (${pidf}) not found; taking pid from 'ps' output."
+ else
+ echo "Ranger PDP Service is not running."
+ exit 0
+ fi
+ fi
+
+ echo "Stopping Ranger PDP Service (pid=${pid})..."
+ kill -15 "$pid"
+
+ for ((i=0; i<NR_ITER_FOR_SHUTDOWN_CHECK; i++)); do
+ sleep $WAIT_TIME_FOR_SHUTDOWN
+ if ps -p "$pid" > /dev/null 2>&1; then
+ echo "Shutdown in progress. Checking again in ${WAIT_TIME_FOR_SHUTDOWN}s..."
+ else
+ break
+ fi
+ done
+
+ if ps -p "$pid" > /dev/null 2>&1; then
+ echo "Graceful stop failed; sending SIGKILL..."
+ kill -9 "$pid"
+ fi
+
+ sleep 1
+ if ps -p "$pid" > /dev/null 2>&1; then
+ echo "kill -9 failed. Process still running. Giving up."
+ exit 1
+ else
+ rm -f "$pidf"
+ echo "Ranger PDP Service with pid ${pid} has been stopped."
+ fi
+ exit 0
+
+elif [ "${action}" == "RESTART" ]; then
+ echo "Restarting Ranger PDP Service..."
+ "${cdir}/ranger-pdp-services.sh" stop
+ "${cdir}/ranger-pdp-services.sh" start
+ exit 0
+
+elif [ "${action}" == "VERSION" ]; then
+ cd "${cdir}/lib" || exit 1
+ java -cp "ranger-util-*.jar" org.apache.ranger.common.RangerVersionInfo
+ exit 0
+
+else
+ echo "Invalid argument [${action}]"
+ echo "Usage: $0 {start | run | stop | restart | version}"
+ exit 1
+fi
diff --git a/pdp/scripts/ranger-pdp.sh b/pdp/scripts/ranger-pdp.sh
new file mode 100644
index 000000000..15d9ebbb0
--- /dev/null
+++ b/pdp/scripts/ranger-pdp.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# 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.
+
+### BEGIN INIT INFO
+# Provides: ranger-pdp
+# Required-Start: $local_fs $remote_fs $network $named $syslog $time
+# Required-Stop: $local_fs $remote_fs $network $named $syslog $time
+# Default-Start: 2 3 4 5
+# Default-Stop:
+# Short-Description: Start/Stop Ranger PDP Server
+### END INIT INFO
+
+LINUX_USER=ranger
+BIN_PATH=/usr/bin
+MOD_NAME=ranger-pdp-services.sh
+pidf=/var/run/ranger/pdp.pid
+pid=""
+
+if [ -f "${pidf}" ]; then
+ pid=$(cat "$pidf")
+fi
+
+case $1 in
+ start)
+ if [ "${pid}" != "" ]; then
+ echo "Ranger PDP Service is already running [pid=${pid}]"
+ exit 1
+ else
+ echo "Starting Ranger PDP Service."
+ /bin/su --login "${LINUX_USER}" -c "${BIN_PATH}/${MOD_NAME} start"
+ fi
+ ;;
+ stop)
+ if [ "${pid}" != "" ]; then
+ echo "Stopping Ranger PDP Service."
+ /bin/su --login "${LINUX_USER}" -c "${BIN_PATH}/${MOD_NAME} stop"
+ else
+ echo "Ranger PDP Service is NOT running."
+ exit 1
+ fi
+ ;;
+ restart)
+ if [ "${pid}" != "" ]; then
+ echo "Stopping Ranger PDP Service."
+ /bin/su --login "${LINUX_USER}" -c "${BIN_PATH}/${MOD_NAME} stop"
+ sleep 10
+ fi
+ echo "Starting Ranger PDP Service."
+ /bin/su --login "${LINUX_USER}" -c "${BIN_PATH}/${MOD_NAME} start"
+ ;;
+ status)
+ if [ "${pid}" != "" ]; then
+ echo "Ranger PDP Service is running [pid=${pid}]"
+ else
+ echo "Ranger PDP Service is NOT running."
+ fi
+ ;;
+ *)
+ echo "Invalid argument [$1]; supported: start | stop | restart | status"
+ exit 1
+ ;;
+esac
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
new file mode 100644
index 000000000..a3027184f
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java
@@ -0,0 +1,295 @@
+/*
+ * 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.pdp;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.valves.AccessLogValve;
+import org.apache.coyote.http2.Http2Protocol;
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.authz.api.RangerAuthzException;
+import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.apache.ranger.pdp.rest.RangerPdpApplication;
+import org.apache.ranger.pdp.security.RangerPdpAuthNFilter;
+import org.apache.ranger.pdp.security.RangerPdpRequestContextFilter;
+import org.apache.tomcat.util.descriptor.web.FilterDef;
+import org.apache.tomcat.util.descriptor.web.FilterMap;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServlet;
+
+import java.io.File;
+
+/**
+ * Main entry point for the Ranger Policy Decision Point (PDP) server.
+ *
+ * <p>Starts an embedded Apache Tomcat instance that:
+ * <ul>
+ * <li>Creates and initialises a {@link RangerEmbeddedAuthorizer} singleton
+ * <li>Exposes the three authorizer methods as REST endpoints under {@code /authz/v1/}
+ * <li>Enforces authentication via {@link RangerPdpAuthNFilter} (Kerberos/JWT/HTTP-Header)
+ * <li>Optionally enables HTTP/2 ({@code Http2Protocol} upgrade on the connector)
+ * </ul>
+ *
+ * <p><b>Startup:</b> {@code java -jar ranger-pdp.jar}
+ * <br>Override config with: {@code -Dranger.pdp.conf.dir=/etc/ranger/pdp}
+ */
+public class RangerPdpServer {
+ private static final Logger LOG = LoggerFactory.getLogger(RangerPdpServer.class);
+
+ private final RangerPdpConfig config;
+ private final RangerPdpStats runtimeStats = new RangerPdpStats();
+ private Tomcat tomcat;
+ private RangerAuthorizer authorizer;
+
+ public RangerPdpServer() {
+ this.config = new RangerPdpConfig();
+ }
+
+ public static void main(String[] args) throws Exception {
+ new RangerPdpServer().start();
+ }
+
+ public void start() throws Exception {
+ LOG.info("Starting Ranger PDP server");
+
+ initAuthorizer();
+ startTomcat();
+ }
+
+ public void stop() {
+ LOG.info("Stopping Ranger PDP server");
+
+ runtimeStats.setAcceptingRequests(false);
+ runtimeStats.setServerStarted(false);
+
+ if (tomcat != null) {
+ try {
+ if (tomcat.getConnector() != null) {
+ tomcat.getConnector().pause();
+ }
+
+ tomcat.stop();
+ tomcat.destroy();
+ } catch (LifecycleException e) {
+ LOG.warn("Error stopping Tomcat", e);
+ }
+ }
+
+ if (authorizer != null) {
+ try {
+ authorizer.close();
+ } catch (RangerAuthzException e) {
+ LOG.warn("Error closing authorizer", e);
+ }
+ }
+ }
+
+ private void initAuthorizer() throws RangerAuthzException {
+ RangerAuthorizer authorizer = new RangerEmbeddedAuthorizer(config.getAuthzProperties());
+
+ authorizer.init();
+
+ this.authorizer = authorizer;
+
+ runtimeStats.setAuthorizerInitialized(true);
+
+ LOG.info("RangerEmbeddedAuthorizer initialised");
+ }
+
+ private void startTomcat() throws Exception {
+ tomcat = new Tomcat();
+
+ tomcat.setConnector(createConnector());
+
+ String docBase = new File(System.getProperty("java.io.tmpdir"), "ranger-pdp-webapps").getAbsolutePath();
+
+ new File(docBase).mkdirs();
+
+ Context ctx = tomcat.addContext("", docBase);
+
+ ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_AUTHORIZER, authorizer);
+ ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_CONFIG, config);
+ ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE, runtimeStats);
+
+ addAuthFilter(ctx);
+ addJerseyServlet(ctx);
+ addStatusEndpoints(ctx);
+ addAccessLogValve();
+
+ Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "ranger-pdp-shutdown"));
+
+ tomcat.start();
+
+ runtimeStats.setServerStarted(true);
+ runtimeStats.setAcceptingRequests(true);
+
+ LOG.info("Ranger PDP server listening on port {} (SSL={}, HTTP/2={})", config.getPort(), config.isSslEnabled(), config.isHttp2Enabled());
+
+ tomcat.getServer().await();
+ }
+
+ /**
+ * Builds the Tomcat connector.
+ *
+ * <p>When SSL is enabled the connector is configured as an HTTPS endpoint.
+ * When HTTP/2 is enabled an {@link Http2Protocol} upgrade protocol is added,
+ * supporting both {@code h2} (over TLS) and {@code h2c} (cleartext upgrade) depending
+ * on whether SSL is enabled.
+ */
+ private Connector createConnector() {
+ Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
+
+ connector.setPort(config.getPort());
+ connector.setProperty("maxThreads", String.valueOf(config.getHttpConnectorMaxThreads()));
+ connector.setProperty("minSpareThreads", String.valueOf(config.getHttpConnectorMinSpareThreads()));
+ connector.setProperty("acceptCount", String.valueOf(config.getHttpConnectorAcceptCount()));
+ connector.setProperty("maxConnections", String.valueOf(config.getHttpConnectorMaxConnections()));
+
+ LOG.info("Configured HTTP connector limits: maxThreads={}, minSpareThreads={}, acceptCount={}, maxConnections={}",
+ config.getHttpConnectorMaxThreads(), config.getHttpConnectorMinSpareThreads(),
+ config.getHttpConnectorAcceptCount(), config.getHttpConnectorMaxConnections());
+
+ if (config.isSslEnabled()) {
+ connector.setSecure(true);
+ connector.setScheme("https");
+ connector.setProperty("SSLEnabled", "true");
+ connector.setProperty("protocol", "org.apache.coyote.http11.Http11NioProtocol");
+ connector.setProperty("keystoreFile", config.getKeystoreFile());
+ connector.setProperty("keystorePass", config.getKeystorePassword());
+ connector.setProperty("keystoreType", config.getKeystoreType());
+ connector.setProperty("sslProtocol", "TLS");
+
+ if (config.isTruststoreEnabled()) {
+ connector.setProperty("truststoreFile", config.getTruststoreFile());
+ connector.setProperty("truststorePass", config.getTruststorePassword());
+ connector.setProperty("truststoreType", config.getTruststoreType());
+ connector.setProperty("clientAuth", "true");
+ }
+ }
+
+ if (config.isHttp2Enabled()) {
+ connector.addUpgradeProtocol(new Http2Protocol());
+
+ LOG.info("HTTP/2 upgrade protocol registered on connector (port={})", config.getPort());
+ }
+
+ return connector;
+ }
+
+ /**
+ * Registers {@link RangerPdpAuthNFilter} on all {@code /authz/*} paths.
+ * Init parameters are forwarded from the server config so the filter can
+ * instantiate and configure the auth handlers.
+ */
+ private void addAuthFilter(Context ctx) {
+ FilterDef reqCtxFilterDef = new FilterDef();
+ FilterMap reqCtxFilterMap = new FilterMap();
+ FilterDef authFilterDef = new FilterDef();
+ FilterMap authFilterMap = new FilterMap();
+
+ reqCtxFilterDef.setFilterName("rangerPdpRequestContextFilter");
+ reqCtxFilterDef.setFilter(new RangerPdpRequestContextFilter());
+
+ reqCtxFilterMap.setFilterName("rangerPdpRequestContextFilter");
+ reqCtxFilterMap.addURLPattern("/*");
+
+ authFilterDef.setFilterName("rangerPdpAuthFilter");
+ authFilterDef.setFilter(new RangerPdpAuthNFilter());
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_TYPES, config.getAuthnTypes());
+
+ // HTTP Header auth
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, Boolean.toString(config.isHeaderAuthnEnabled()));
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, config.getHeaderAuthnUsername());
+
+ // JWT bearer token auth
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, Boolean.toString(config.isJwtAuthnEnabled()));
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, config.getJwtProviderUrl());
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, config.getJwtPublicKey());
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, config.getJwtCookieName());
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, config.getJwtAudiences());
+
+ // Kerberos / SPNEGO
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED, Boolean.toString(config.isKerberosAuthnEnabled()));
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL, config.getSpnegoPrincipal());
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB, config.getSpnegoKeytab());
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, config.getKerberosNameRules());
+ authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, String.valueOf(config.getKerberosTokenValiditySeconds()));
+
+ authFilterMap.setFilterName("rangerPdpAuthFilter");
+ authFilterMap.addURLPattern("/authz/*");
+
+ ctx.addFilterDef(reqCtxFilterDef);
+ ctx.addFilterMap(reqCtxFilterMap);
+ ctx.addFilterDef(authFilterDef);
+ ctx.addFilterMap(authFilterMap);
+ }
+
+ /**
+ * Registers the Jersey {@link ServletContainer} backed by {@link RangerPdpApplication}.
+ * The Jersey application binds the {@link RangerAuthorizer} singleton via HK2 so
+ * that it can be injected into {@link org.apache.ranger.pdp.rest.RangerPdpREST}.
+ */
+ private void addJerseyServlet(Context ctx) {
+ RangerPdpApplication jerseyApp = new RangerPdpApplication(authorizer, config);
+ Servlet jerseyServlet = new ServletContainer(jerseyApp);
+
+ Tomcat.addServlet(ctx, "jerseyServlet", jerseyServlet).setLoadOnStartup(1);
+
+ ctx.addServletMappingDecoded("/authz/*", "jerseyServlet");
+ }
+
+ private void addStatusEndpoints(Context ctx) {
+ HttpServlet liveServlet = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.LIVE);
+ HttpServlet readyServlet = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.READY);
+ HttpServlet metricsServlet = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.METRICS);
+
+ Tomcat.addServlet(ctx, "pdpLiveServlet", liveServlet).setLoadOnStartup(1);
+ Tomcat.addServlet(ctx, "pdpReadyServlet", readyServlet).setLoadOnStartup(1);
+ Tomcat.addServlet(ctx, "pdpMetricsServlet", metricsServlet).setLoadOnStartup(1);
+
+ ctx.addServletMappingDecoded("/health/live", "pdpLiveServlet");
+ ctx.addServletMappingDecoded("/health/ready", "pdpReadyServlet");
+ ctx.addServletMappingDecoded("/metrics", "pdpMetricsServlet");
+ }
+
+ private void addAccessLogValve() {
+ String logDir = config.getLogDir();
+
+ new File(logDir).mkdirs();
+
+ AccessLogValve valve = new AccessLogValve();
+
+ valve.setDirectory(logDir);
+ valve.setPrefix("ranger-pdp-access");
+ valve.setSuffix(".log");
+ valve.setPattern("%h %l %u %t \"%r\" %s %b %D");
+ valve.setRotatable(true);
+
+ tomcat.getHost().getPipeline().addValve(valve);
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java
new file mode 100644
index 000000000..c11c42d8e
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java
@@ -0,0 +1,113 @@
+/*
+ * 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.pdp;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class RangerPdpStats {
+ private final AtomicBoolean serverStarted = new AtomicBoolean(false);
+ private final AtomicBoolean authorizerInitialized = new AtomicBoolean(false);
+ private final AtomicBoolean acceptingRequests = new AtomicBoolean(false);
+
+ private final AtomicLong totalRequests = new AtomicLong(0);
+ private final AtomicLong totalAuthzSuccess = new AtomicLong(0);
+ private final AtomicLong totalAuthzBadRequest = new AtomicLong(0);
+ private final AtomicLong totalAuthzErrors = new AtomicLong(0);
+ private final AtomicLong totalAuthFailures = new AtomicLong(0);
+ private final AtomicLong totalLatencyNanos = new AtomicLong(0);
+
+ public boolean isServerStarted() {
+ return serverStarted.get();
+ }
+
+ public void setServerStarted(boolean value) {
+ serverStarted.set(value);
+ }
+
+ public boolean isAuthorizerInitialized() {
+ return authorizerInitialized.get();
+ }
+
+ public void setAuthorizerInitialized(boolean value) {
+ authorizerInitialized.set(value);
+ }
+
+ public boolean isAcceptingRequests() {
+ return acceptingRequests.get();
+ }
+
+ public void setAcceptingRequests(boolean value) {
+ acceptingRequests.set(value);
+ }
+
+ public void recordRequestSuccess(long elapsedNanos) {
+ totalRequests.incrementAndGet();
+ totalAuthzSuccess.incrementAndGet();
+ totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos));
+ }
+
+ public void recordRequestBadRequest(long elapsedNanos) {
+ totalRequests.incrementAndGet();
+ totalAuthzBadRequest.incrementAndGet();
+ totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos));
+ }
+
+ public void recordRequestError(long elapsedNanos) {
+ totalRequests.incrementAndGet();
+ totalAuthzErrors.incrementAndGet();
+ totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos));
+ }
+
+ public void recordAuthFailure(long elapsedNanos) {
+ totalRequests.incrementAndGet();
+ totalAuthFailures.incrementAndGet();
+ totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos));
+ }
+
+ public long getTotalRequests() {
+ return totalRequests.get();
+ }
+
+ public long getTotalAuthzSuccess() {
+ return totalAuthzSuccess.get();
+ }
+
+ public long getTotalAuthzBadRequest() {
+ return totalAuthzBadRequest.get();
+ }
+
+ public long getTotalAuthzErrors() {
+ return totalAuthzErrors.get();
+ }
+
+ public long getTotalAuthFailures() {
+ return totalAuthFailures.get();
+ }
+
+ public long getTotalLatencyNanos() {
+ return totalLatencyNanos.get();
+ }
+
+ public long getAverageLatencyMs() {
+ long requests = totalRequests.get();
+ return requests > 0 ? (totalLatencyNanos.get() / requests) / 1_000_000L : 0L;
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java
new file mode 100644
index 000000000..9725909c8
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java
@@ -0,0 +1,129 @@
+/*
+ * 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.pdp;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.MediaType;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class RangerPdpStatusServlet extends HttpServlet {
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private final RangerPdpStats runtimeState;
+ private final Mode mode;
+
+ public enum Mode { LIVE, READY, METRICS }
+
+ public RangerPdpStatusServlet(RangerPdpStats runtimeState, Mode mode) {
+ this.runtimeState = runtimeState;
+ this.mode = mode;
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ switch (mode) {
+ case LIVE:
+ writeLive(resp);
+ break;
+ case READY:
+ writeReady(req, resp);
+ break;
+ case METRICS:
+ writeMetrics(req, resp);
+ break;
+ default:
+ resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ }
+ }
+
+ private void writeLive(HttpServletResponse resp) throws IOException {
+ Map<String, Object> payload = new LinkedHashMap<>();
+
+ payload.put("status", runtimeState.isServerStarted() ? "UP" : "DOWN");
+ payload.put("service", "ranger-pdp");
+ payload.put("live", runtimeState.isServerStarted());
+
+ resp.setStatus(runtimeState.isServerStarted() ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ resp.setContentType(MediaType.APPLICATION_JSON);
+
+ MAPPER.writeValue(resp.getOutputStream(), payload);
+ }
+
+ private void writeReady(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ boolean ready = runtimeState.isServerStarted() && runtimeState.isAuthorizerInitialized() && runtimeState.isAcceptingRequests();
+
+ Map<String, Object> payload = new LinkedHashMap<>();
+
+ payload.put("status", ready ? "READY" : "NOT_READY");
+ payload.put("service", "ranger-pdp");
+ payload.put("ready", ready);
+ payload.put("authorizerInitialized", runtimeState.isAuthorizerInitialized());
+ payload.put("acceptingRequests", runtimeState.isAcceptingRequests());
+ payload.put("loadedServicesCount", getLoadedServicesCount(req));
+
+ resp.setStatus(ready ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE);
+ resp.setContentType(MediaType.APPLICATION_JSON);
+
+ MAPPER.writeValue(resp.getOutputStream(), payload);
+ }
+
+ private void writeMetrics(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+ StringBuilder sb = new StringBuilder(512);
+
+ sb.append("# TYPE ranger_pdp_requests_total counter\n");
+ sb.append("ranger_pdp_requests_total ").append(runtimeState.getTotalRequests()).append('\n');
+ sb.append("# TYPE ranger_pdp_requests_success_total counter\n");
+ sb.append("ranger_pdp_requests_success_total ").append(runtimeState.getTotalAuthzSuccess()).append('\n');
+ sb.append("# TYPE ranger_pdp_requests_bad_request_total counter\n");
+ sb.append("ranger_pdp_requests_bad_request_total ").append(runtimeState.getTotalAuthzBadRequest()).append('\n');
+ sb.append("# TYPE ranger_pdp_requests_error_total counter\n");
+ sb.append("ranger_pdp_requests_error_total ").append(runtimeState.getTotalAuthzErrors()).append('\n');
+ sb.append("# TYPE ranger_pdp_auth_failures_total counter\n");
+ sb.append("ranger_pdp_auth_failures_total ").append(runtimeState.getTotalAuthFailures()).append('\n');
+ sb.append("# TYPE ranger_pdp_request_latency_avg_ms gauge\n");
+ sb.append("ranger_pdp_request_latency_avg_ms ").append(runtimeState.getAverageLatencyMs()).append('\n');
+ sb.append("# TYPE ranger_pdp_loaded_services_count gauge\n");
+ sb.append("ranger_pdp_loaded_services_count ").append(getLoadedServicesCount(req)).append('\n');
+
+ resp.setStatus(HttpServletResponse.SC_OK);
+ resp.setContentType("text/plain; version=0.0.4");
+
+ resp.getWriter().write(sb.toString());
+ }
+
+ private int getLoadedServicesCount(HttpServletRequest req) {
+ ServletContext context = req.getServletContext();
+ Object authorizer = context != null ? context.getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_AUTHORIZER) : null;
+ Set<String> services = authorizer instanceof RangerEmbeddedAuthorizer ? ((RangerEmbeddedAuthorizer) authorizer).getLoadedServices() : null;
+
+ return services != null ? services.size() : 0;
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
new file mode 100644
index 000000000..11aea39ea
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java
@@ -0,0 +1,281 @@
+/*
+ * 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.pdp.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.plugin.util.XMLUtils;
+import org.ietf.jgss.GSSCredential;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.util.Properties;
+
+/**
+ * Reads Ranger PDP configuration from {@code ranger-pdp-default.xml} (classpath)
+ * overridden by {@code ranger-pdp-site.xml} (classpath or filesystem).
+ *
+ * <p>Both files use the Hadoop {@code <configuration>} XML format, consistent
+ * with other Ranger server modules (tagsync, kms, etc.).
+ * The format is parsed directly using the JDK DOM API to avoid an early
+ * class-load dependency on Hadoop's {@code Configuration} class.
+ *
+ * <p>Authentication property names:
+ * <ul>
+ * <li>Kerberos/SPNEGO: {@code ranger.pdp.authn.kerberos.*}
+ * <li>JWT bearer token: {@code ranger.pdp.authn.jwt.*}
+ * <li>HTTP header: {@code ranger.pdp.authn.header.*}
+ * </ul>
+ */
+public class RangerPdpConfig {
+ private static final Logger LOG = LoggerFactory.getLogger(RangerPdpConfig.class);
+
+ private static final String DEFAULT_CONFIG_FILE = "ranger-pdp-default.xml";
+ private static final String SITE_CONFIG_FILE = "ranger-pdp-site.xml";
+
+ private final Properties props = new Properties();
+
+ public RangerPdpConfig() {
+ loadFromClasspath(DEFAULT_CONFIG_FILE);
+ loadFromClasspath(SITE_CONFIG_FILE);
+
+ String confDir = System.getProperty(RangerPdpConstants.PROP_CONF_DIR, "");
+
+ if (StringUtils.isNotBlank(confDir)) {
+ loadFromFile(new File(confDir, SITE_CONFIG_FILE));
+ }
+
+ applySystemPropertyOverrides();
+
+ LOG.info("RangerPdpConfig initialized (conf.dir={})", confDir);
+ }
+
+ public int getPort() {
+ return getInt(RangerPdpConstants.PROP_PORT, 6500);
+ }
+
+ public String getLogDir() {
+ return get(RangerPdpConstants.PROP_LOG_DIR, "/var/log/ranger/pdp");
+ }
+
+ public boolean isSslEnabled() {
+ return getBoolean(RangerPdpConstants.PROP_SSL_ENABLED, false);
+ }
+
+ public String getKeystoreFile() {
+ return get(RangerPdpConstants.PROP_SSL_KEYSTORE_FILE, "");
+ }
+
+ public String getKeystorePassword() {
+ return get(RangerPdpConstants.PROP_SSL_KEYSTORE_PASSWORD, "");
+ }
+
+ public String getKeystoreType() {
+ return get(RangerPdpConstants.PROP_SSL_KEYSTORE_TYPE, "JKS");
+ }
+
+ public boolean isTruststoreEnabled() {
+ return getBoolean(RangerPdpConstants.PROP_SSL_TRUSTSTORE_ENABLED, false);
+ }
+
+ public String getTruststoreFile() {
+ return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_FILE, "");
+ }
+
+ public String getTruststorePassword() {
+ return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_PASSWORD, "");
+ }
+
+ public String getTruststoreType() {
+ return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_TYPE, "JKS");
+ }
+
+ public boolean isHttp2Enabled() {
+ return getBoolean(RangerPdpConstants.PROP_HTTP2_ENABLED, true);
+ }
+
+ public int getHttpConnectorMaxThreads() {
+ return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_THREADS, 200);
+ }
+
+ public int getHttpConnectorMinSpareThreads() {
+ return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MIN_SPARE_THREADS, 20);
+ }
+
+ public int getHttpConnectorAcceptCount() {
+ return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_ACCEPT_COUNT, 100);
+ }
+
+ public int getHttpConnectorMaxConnections() {
+ return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_CONNECTIONS, 10000);
+ }
+
+ public String getAuthnTypes() {
+ return get(RangerPdpConstants.PROP_AUTHN_TYPES, "header,jwt,kerberos");
+ }
+
+ // --- HTTP Header auth ---
+ public boolean isHeaderAuthnEnabled() {
+ return getBoolean(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, false);
+ }
+
+ public String getHeaderAuthnUsername() {
+ return get(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Forwarded-User");
+ }
+
+ // --- JWT bearer token auth ---
+ public boolean isJwtAuthnEnabled() {
+ return getBoolean(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, false);
+ }
+
+ public String getJwtProviderUrl() {
+ return get(RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, "");
+ }
+
+ public String getJwtPublicKey() {
+ return get(RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, "");
+ }
+
+ public String getJwtCookieName() {
+ return get(RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, "hadoop-jwt");
+ }
+
+ public String getJwtAudiences() {
+ return get(RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, "");
+ }
+
+ // --- Kerberos / SPNEGO ---
+ public boolean isKerberosAuthnEnabled() {
+ return getBoolean(RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED, false);
+ }
+
+ public String getSpnegoPrincipal() {
+ return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL, "");
+ }
+
+ public String getSpnegoKeytab() {
+ return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB, "");
+ }
+
+ public int getKerberosTokenValiditySeconds() {
+ return getInt(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, GSSCredential.INDEFINITE_LIFETIME);
+ }
+
+ public String getKerberosNameRules() {
+ return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, "DEFAULT");
+ }
+
+ /**
+ * Returns all properties for forwarding to {@code RangerEmbeddedAuthorizer}.
+ */
+ public Properties getAuthzProperties() {
+ return new Properties(props);
+ }
+
+ public String get(String key, String defaultValue) {
+ String val = props.getProperty(key);
+
+ return StringUtils.isNotBlank(val) ? val.trim() : defaultValue;
+ }
+
+ public int getInt(String key, int defaultValue) {
+ String val = props.getProperty(key);
+
+ if (StringUtils.isNotBlank(val)) {
+ try {
+ return Integer.parseInt(val.trim());
+ } catch (NumberFormatException e) {
+ LOG.warn("Invalid integer for {}: '{}'; using default {}", key, val, defaultValue);
+ }
+ }
+
+ return defaultValue;
+ }
+
+ public boolean getBoolean(String key, boolean defaultValue) {
+ String val = props.getProperty(key);
+
+ return StringUtils.isNotBlank(val) ? Boolean.parseBoolean(val.trim()) : defaultValue;
+ }
+
+ private void loadFromClasspath(String resourceName) {
+ try (InputStream in = getClass().getClassLoader().getResourceAsStream(resourceName)) {
+ if (in != null) {
+ parseHadoopXml(in, resourceName);
+ } else {
+ LOG.debug("Config resource not found on classpath: {}", resourceName);
+ }
+ } catch (IOException e) {
+ LOG.warn("Failed to close stream for classpath resource: {}", resourceName, e);
+ }
+ }
+
+ private void loadFromFile(File file) {
+ if (!file.exists() || !file.isFile()) {
+ LOG.debug("Config file not found: {}", file);
+ return;
+ }
+
+ try (InputStream in = Files.newInputStream(file.toPath())) {
+ parseHadoopXml(in, file.getAbsolutePath());
+ } catch (IOException e) {
+ LOG.warn("Failed to read config file: {}", file, e);
+ }
+ }
+
+ /**
+ * Parses a Hadoop-style {@code <configuration>} XML document and merges all
+ * {@code <property>} entries into {@link #props}. Later entries override earlier
+ * ones, matching Hadoop's own override semantics.
+ *
+ * <pre>
+ * {@code
+ * <configuration>
+ * <property>
+ * <name>some.key</name>
+ * <value>some-value</value>
+ * </property>
+ * </configuration>
+ * }
+ * </pre>
+ */
+ private void parseHadoopXml(InputStream in, String source) {
+ LOG.info("Loading from {}. Properties count {}", source, props.size());
+
+ XMLUtils.loadConfig(in, props);
+
+ LOG.info("Loaded from {}. Properties count {}", source, props.size());
+ }
+
+ /**
+ * Apply JVM system-property overrides for operationally sensitive keys so Kubernetes
+ * (or any orchestrator) can drive runtime config with JAVA_OPTS/-D flags.
+ */
+ private void applySystemPropertyOverrides() {
+ for (String key : System.getProperties().stringPropertyNames()) {
+ if (key.startsWith(RangerPdpConstants.PROP_PDP_PREFIX) || key.startsWith(RangerPdpConstants.PROP_AUTHZ_PREFIX)) {
+ props.setProperty(key, System.getProperty(key));
+ }
+ }
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java
new file mode 100644
index 000000000..c1a431351
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.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.pdp.config;
+
+public final class RangerPdpConstants {
+ private RangerPdpConstants() {
+ // no instances
+ }
+
+ // Servlet context attributes
+ public static final String SERVLET_CTX_ATTR_CONFIG = "ranger.pdp.config";
+ public static final String SERVLET_CTX_ATTR_AUTHORIZER = "ranger.pdp.authorizer";
+ public static final String SERVLET_CTX_ATTR_RUNTIME_STATE = "ranger.pdp.runtime.state";
+
+ // Request attributes set by auth filter
+ public static final String ATTR_AUTHENTICATED_USER = "ranger.pdp.authenticated.user";
+ public static final String ATTR_AUTHN_TYPE = "ranger.pdp.authn.type";
+
+ // Server
+ public static final String PROP_CONF_DIR = "ranger.pdp.conf.dir";
+ public static final String PROP_PORT = "ranger.pdp.port";
+ public static final String PROP_LOG_DIR = "ranger.pdp.log.dir";
+
+ // SSL/TLS
+ public static final String PROP_SSL_ENABLED = "ranger.pdp.ssl.enabled";
+ public static final String PROP_SSL_KEYSTORE_FILE = "ranger.pdp.ssl.keystore.file";
+ public static final String PROP_SSL_KEYSTORE_PASSWORD = "ranger.pdp.ssl.keystore.password";
+ public static final String PROP_SSL_KEYSTORE_TYPE = "ranger.pdp.ssl.keystore.type";
+ public static final String PROP_SSL_TRUSTSTORE_ENABLED = "ranger.pdp.ssl.truststore.enabled";
+ public static final String PROP_SSL_TRUSTSTORE_FILE = "ranger.pdp.ssl.truststore.file";
+ public static final String PROP_SSL_TRUSTSTORE_PASSWORD = "ranger.pdp.ssl.truststore.password";
+ public static final String PROP_SSL_TRUSTSTORE_TYPE = "ranger.pdp.ssl.truststore.type";
+
+ // HTTP/2
+ public static final String PROP_HTTP2_ENABLED = "ranger.pdp.http2.enabled";
+
+ // HTTP connector limits
+ public static final String PROP_HTTP_CONNECTOR_MAX_THREADS = "ranger.pdp.http.connector.maxThreads";
+ public static final String PROP_HTTP_CONNECTOR_MIN_SPARE_THREADS = "ranger.pdp.http.connector.minSpareThreads";
+ public static final String PROP_HTTP_CONNECTOR_ACCEPT_COUNT = "ranger.pdp.http.connector.acceptCount";
+ public static final String PROP_HTTP_CONNECTOR_MAX_CONNECTIONS = "ranger.pdp.http.connector.maxConnections";
+
+ // Authentication types
+ public static final String PROP_AUTHN_PREFIX = "ranger.pdp.authn.";
+ public static final String PROP_AUTHN_TYPES = PROP_AUTHN_PREFIX + "types";
+
+ // HTTP header auth
+ public static final String PROP_AUTHN_HEADER_PREFIX = PROP_AUTHN_PREFIX + "header.";
+ public static final String PROP_AUTHN_HEADER_ENABLED = PROP_AUTHN_HEADER_PREFIX + "enabled";
+ public static final String PROP_AUTHN_HEADER_USERNAME = PROP_AUTHN_HEADER_PREFIX + "username";
+
+ // JWT auth
+ public static final String PROP_AUTHN_JWT_PREFIX = PROP_AUTHN_PREFIX + "jwt.";
+ public static final String PROP_AUTHN_JWT_ENABLED = PROP_AUTHN_JWT_PREFIX + "enabled";
+ public static final String PROP_AUTHN_JWT_PROVIDER_URL = PROP_AUTHN_JWT_PREFIX + "provider.url";
+ public static final String PROP_AUTHN_JWT_PUBLIC_KEY = PROP_AUTHN_JWT_PREFIX + "public.key";
+ public static final String PROP_AUTHN_JWT_COOKIE_NAME = PROP_AUTHN_JWT_PREFIX + "cookie.name";
+ public static final String PROP_AUTHN_JWT_AUDIENCES = PROP_AUTHN_JWT_PREFIX + "audiences";
+
+ // Kerberos/SPNEGO auth
+ public static final String PROP_AUTHN_KERBEROS_PREFIX = PROP_AUTHN_PREFIX + "kerberos.";
+ public static final String PROP_AUTHN_KERBEROS_ENABLED = PROP_AUTHN_KERBEROS_PREFIX + "enabled";
+ public static final String PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL = PROP_AUTHN_KERBEROS_PREFIX + "spnego.principal";
+ public static final String PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB = PROP_AUTHN_KERBEROS_PREFIX + "spnego.keytab";
+ public static final String PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY = PROP_AUTHN_KERBEROS_PREFIX + "token.valid.seconds";
+ public static final String PROP_AUTHN_KERBEROS_NAME_RULES = PROP_AUTHN_KERBEROS_PREFIX + "name.rules";
+
+ // Authorizer/audit properties referenced by PDP code
+ public static final String PROP_AUTHZ_POLICY_CACHE_DIR = "ranger.authz.default.policy.cache.dir";
+ public static final String PROP_AUTHZ_PREFIX = "ranger.authz.";
+ public static final String PROP_PDP_PREFIX = "ranger.pdp.";
+ public static final String PROP_PDP_SERVICE_PREFIX = PROP_PDP_PREFIX + "service.";
+
+ // delegation users
+ public static final String PROP_SUFFIX_DELEGATION_USERS = ".delegation.users";
+ public static final String WILDCARD_SERVICE_NAME = "*";
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java b/pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java
new file mode 100644
index 000000000..d06c1e670
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java
@@ -0,0 +1,45 @@
+/*
+ * 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.pdp.model;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import javax.ws.rs.core.Response;
+
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ErrorResponse {
+ private final String code;
+ private final String message;
+
+ public ErrorResponse(Response.Status status, String message) {
+ this.code = status.name();
+ this.message = message;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpApplication.java b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpApplication.java
new file mode 100644
index 000000000..42371b452
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpApplication.java
@@ -0,0 +1,62 @@
+/*
+ * 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.pdp.rest;
+
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.glassfish.hk2.utilities.binding.AbstractBinder;
+import org.glassfish.jersey.jackson.JacksonFeature;
+import org.glassfish.jersey.server.ResourceConfig;
+
+/**
+ * Jersey 2.x {@link ResourceConfig} for the Ranger PDP application.
+ *
+ * <p>Registers:
+ * <ul>
+ * <li>{@link JacksonFeature} – Jackson 2.x JSON provider (honours all {@code @Json*}
+ * annotations on the {@code authz-api} model classes)
+ * <li>{@link AuthorizerBinder} – HK2 binder that makes the {@link RangerAuthorizer}
+ * instance injectable into {@link RangerPdpREST} via {@code @Inject}
+ * <li>{@link RangerPdpREST} – the JAX-RS resource class
+ * </ul>
+ */
+public class RangerPdpApplication extends ResourceConfig {
+ public RangerPdpApplication(RangerAuthorizer authorizer, RangerPdpConfig config) {
+ register(JacksonFeature.class);
+ register(new AuthorizerBinder(authorizer, config));
+ register(RangerPdpREST.class);
+ }
+
+ private static class AuthorizerBinder extends AbstractBinder {
+ private final RangerAuthorizer authorizer;
+ private final RangerPdpConfig config;
+
+ AuthorizerBinder(RangerAuthorizer authorizer, RangerPdpConfig config) {
+ this.authorizer = authorizer;
+ this.config = config;
+ }
+
+ @Override
+ protected void configure() {
+ bind(authorizer).to(RangerAuthorizer.class);
+ bind(config).to(RangerPdpConfig.class);
+ }
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java
new file mode 100644
index 000000000..1f447f706
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java
@@ -0,0 +1,491 @@
+/*
+ * 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.pdp.rest;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.authz.api.RangerAuthzException;
+import org.apache.ranger.authz.model.RangerAccessInfo;
+import org.apache.ranger.authz.model.RangerAuthzRequest;
+import org.apache.ranger.authz.model.RangerAuthzResult;
+import org.apache.ranger.authz.model.RangerMultiAuthzRequest;
+import org.apache.ranger.authz.model.RangerMultiAuthzResult;
+import org.apache.ranger.authz.model.RangerResourceInfo;
+import org.apache.ranger.authz.model.RangerResourcePermissions;
+import org.apache.ranger.authz.model.RangerResourcePermissionsRequest;
+import org.apache.ranger.authz.model.RangerUserInfo;
+import org.apache.ranger.pdp.RangerPdpStats;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.apache.ranger.pdp.model.ErrorResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
+import static javax.ws.rs.core.Response.Status.OK;
+import static javax.ws.rs.core.Response.Status.UNAUTHORIZED;
+import static org.apache.ranger.authz.api.RangerAuthzApiErrorCode.INVALID_REQUEST_USER_INFO_MISSING;
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_PDP_SERVICE_PREFIX;
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_SUFFIX_DELEGATION_USERS;
+import static org.apache.ranger.pdp.config.RangerPdpConstants.WILDCARD_SERVICE_NAME;
+
+/**
+ * REST resource that exposes the three core {@link RangerAuthorizer} methods over HTTP.
+ *
+ * <p>All endpoints are under {@code /authz/v1} and produce/consume {@code application/json}.
+ * Authentication is enforced upstream by {@link org.apache.ranger.pdp.security.RangerPdpAuthNFilter}; the authenticated
+ * caller's identity is read from the {@link RangerPdpConstants#ATTR_AUTHENTICATED_USER}
+ * request attribute.
+ *
+ * <table border="1">
+ * <tr><th>Method</th><th>Path</th><th>Request body</th><th>Response body</th></tr>
+ * <tr><td>POST</td><td>/authz/v1/authorize</td>
+ * <td>{@link RangerAuthzRequest}</td><td>{@link RangerAuthzResult}</td></tr>
+ * <tr><td>POST</td><td>/authz/v1/authorizeMulti</td>
+ * <td>{@link RangerMultiAuthzRequest}</td><td>{@link RangerMultiAuthzResult}</td></tr>
+ * <tr><td>POST</td><td>/authz/v1/permissions</td>
+ * <td>{@link RangerResourcePermissionsRequest}</td>
+ * <td>{@link RangerResourcePermissions}</td></tr>
+ * </table>
+ */
+@Path("/v1")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Singleton
+public class RangerPdpREST {
+ private static final Logger LOG = LoggerFactory.getLogger(RangerPdpREST.class);
+
+ private static final Response RESPONSE_OK = Response.ok().build();
+
+ private final Map<String, Set<String>> delegationUsersByService = new HashMap<>();
+
+ @Inject
+ private RangerAuthorizer authorizer;
+
+ @Inject
+ private RangerPdpConfig config;
+
+ @Context
+ private ServletContext servletContext;
+
+ @PostConstruct
+ public void initialize() {
+ initializeDelegationUsers();
+ }
+
+ /**
+ * Evaluates a single access request.
+ *
+ * @param request the authorization request
+ * @return {@code 200 OK} with {@link RangerAuthzResult}, or {@code 400} / {@code 500} on error
+ */
+ @POST
+ @Path("/authorize")
+ public Response authorize(RangerAuthzRequest request, @Context HttpServletRequest httpRequest) {
+ long startNanos = System.nanoTime();
+ Response ret = null;
+
+ try {
+ String requestId = request != null ? request.getRequestId() : null;
+ String caller = getAuthenticatedUser(httpRequest);
+ String serviceName = getServiceName(request);
+ RangerUserInfo user = request != null ? request.getUser() : null;
+ RangerAccessInfo access = request != null ? request.getAccess() : null;
+
+ LOG.debug("==> authorize(requestId={}, caller={}, serviceName={})", requestId, caller, serviceName);
+
+ ret = validateCaller(caller, user, access, serviceName);
+
+ if (isStatusOk(ret)) {
+ try {
+ RangerAuthzResult result = authorizer.authorize(request);
+
+ ret = Response.ok(result).build();
+ } catch (RangerAuthzException e) {
+ LOG.warn("authorize(requestId={}): authorization error; caller={}", requestId, caller, e);
+
+ ret = badRequest(e);
+ } catch (Exception e) {
+ LOG.error("authorize(requestId={}): internal error; caller={}", requestId, caller, e);
+
+ ret = serverError();
+ }
+ }
+
+ LOG.debug("<== authorize(requestId={}, caller={}, serviceName={}): ret={}", requestId, caller, serviceName, ret != null ? ret.getStatus() : null);
+ } finally {
+ recordRequestMetrics(ret, startNanos, httpRequest);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Evaluates multiple access requests in a single call.
+ *
+ * @param request the multi-access authorization request
+ * @return {@code 200 OK} with {@link RangerMultiAuthzResult}, or {@code 400} / {@code 500} on error
+ */
+ @POST
+ @Path("/authorizeMulti")
+ public Response authorizeMulti(RangerMultiAuthzRequest request, @Context HttpServletRequest httpRequest) {
+ long startNanos = System.nanoTime();
+ Response ret = null;
+
+ try {
+ String requestId = request != null ? request.getRequestId() : null;
+ String caller = getAuthenticatedUser(httpRequest);
+ String serviceName = getServiceName(request);
+ RangerUserInfo user = request != null ? request.getUser() : null;
+ List<RangerAccessInfo> accesses = request != null ? request.getAccesses() : null;
+
+ LOG.debug("==> authorizeMulti(requestId={}, caller={}, serviceName={})", requestId, caller, serviceName);
+
+ ret = validateCaller(caller, user, accesses, serviceName);
+
+ if (isStatusOk(ret)) {
+ try {
+ RangerMultiAuthzResult result = authorizer.authorize(request);
+
+ ret = Response.ok(result).build();
+ } catch (RangerAuthzException e) {
+ LOG.warn("authorizeMulti(requestId={}): authorization error; caller={}", requestId, caller, e);
+
+ ret = badRequest(e);
+ } catch (Exception e) {
+ LOG.error("authorizeMulti(requestId={}): internal error; caller={}", requestId, caller, e);
+
+ ret = serverError();
+ }
+ }
+
+ LOG.debug("<== authorizeMulti(requestId={}, caller={}, serviceName={}): ret={}", requestId, caller, serviceName, ret != null ? ret.getStatus() : null);
+ } finally {
+ recordRequestMetrics(ret, startNanos, httpRequest);
+ }
+
+ return ret;
+ }
+
+ /**
+ * Returns the effective permissions for a resource, broken down by user/group/role.
+ *
+ * @param request wrapper containing the resource info and access context
+ * @return {@code 200 OK} with {@link RangerResourcePermissions}, or {@code 400} / {@code 500} on error
+ */
+ @POST
+ @Path("/permissions")
+ public Response getResourcePermissions(RangerResourcePermissionsRequest request, @Context HttpServletRequest httpRequest) {
+ long startNanos = System.nanoTime();
+ Response ret = null;
+
+ try {
+ String caller = getAuthenticatedUser(httpRequest);
+ String serviceName = getServiceName(request);
+
+ LOG.debug("==> getResourcePermissions(caller={}, serviceName={})", caller, serviceName);
+
+ ret = validateCaller(caller, serviceName);
+
+ if (isStatusOk(ret)) {
+ try {
+ RangerResourcePermissions result = authorizer.getResourcePermissions(request);
+
+ ret = Response.ok(result).build();
+ } catch (RangerAuthzException e) {
+ LOG.warn("getResourcePermissions(): validation error; caller={}", caller, e);
+
+ ret = badRequest(e);
+ } catch (Exception e) {
+ LOG.error("getResourcePermissions(): unexpected error; caller={}", caller, e);
+
+ ret = serverError();
+ }
+ }
+
+ LOG.debug("<== getResourcePermissions(caller={}, serviceName={}): ret={}", caller, serviceName, ret != null ? ret.getStatus() : null);
+ } finally {
+ recordRequestMetrics(ret, startNanos, httpRequest);
+ }
+
+ return ret;
+ }
+
+ private String getAuthenticatedUser(HttpServletRequest httpRequest) {
+ Object user = httpRequest.getAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER);
+
+ return user != null ? user.toString() : null;
+ }
+
+ private static String getServiceName(RangerAuthzRequest request) {
+ return request != null && request.getContext() != null ? request.getContext().getServiceName() : null;
+ }
+
+ private static String getServiceName(RangerMultiAuthzRequest request) {
+ return request != null && request.getContext() != null ? request.getContext().getServiceName() : null;
+ }
+
+ private static String getServiceName(RangerResourcePermissionsRequest request) {
+ return request != null && request.getContext() != null ? request.getContext().getServiceName() : null;
+ }
+
+ private Response validateCaller(String caller, RangerUserInfo user, RangerAccessInfo access, String serviceName) {
+ final Response ret;
+
+ if (StringUtils.isBlank(caller)) {
+ ret = Response.status(UNAUTHORIZED)
+ .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required"))
+ .build();
+ } else if (user == null || StringUtils.isBlank(user.getName())) {
+ ret = Response.status(BAD_REQUEST)
+ .entity(new ErrorResponse(BAD_REQUEST, INVALID_REQUEST_USER_INFO_MISSING.getMessage()))
+ .build();
+ } else {
+ boolean needsDelegation = isDelegationNeeded(caller, user) || isDelegationNeeded(access);
+
+ if (needsDelegation) {
+ if (!isDelegationUserForService(serviceName, caller)) {
+ LOG.info("{} is not a delegation user in service {}", caller, serviceName);
+
+ ret = Response.status(FORBIDDEN)
+ .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized"))
+ .build();
+ } else {
+ ret = RESPONSE_OK;
+ }
+ } else {
+ ret = RESPONSE_OK;
+ }
+ }
+
+ return ret;
+ }
+
+ private Response validateCaller(String caller, RangerUserInfo user, List<RangerAccessInfo> accesses, String serviceName) {
+ final Response ret;
+
+ if (StringUtils.isBlank(caller)) {
+ ret = Response.status(UNAUTHORIZED)
+ .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required"))
+ .build();
+ } else if (user == null || StringUtils.isBlank(user.getName())) {
+ ret = Response.status(BAD_REQUEST)
+ .entity(new ErrorResponse(BAD_REQUEST, INVALID_REQUEST_USER_INFO_MISSING.getMessage()))
+ .build();
+ } else {
+ boolean needsDelegation = isDelegationNeeded(caller, user) || isDelegationNeeded(accesses);
+
+ if (needsDelegation) {
+ if (!isDelegationUserForService(serviceName, caller)) {
+ LOG.info("{} is not a delegation user in service {}", caller, serviceName);
+
+ ret = Response.status(FORBIDDEN)
+ .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized"))
+ .build();
+ } else {
+ ret = RESPONSE_OK;
+ }
+ } else {
+ ret = RESPONSE_OK;
+ }
+ }
+
+ return ret;
+ }
+
+ private Response validateCaller(String caller, String serviceName) {
+ final Response ret;
+
+ if (StringUtils.isBlank(caller)) {
+ ret = Response.status(UNAUTHORIZED)
+ .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required"))
+ .build();
+ } else if (!isDelegationUserForService(serviceName, caller)) {
+ LOG.info("{} is not a delegation user in service {}", caller, serviceName);
+
+ ret = Response.status(FORBIDDEN)
+ .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized"))
+ .build();
+ } else {
+ ret = RESPONSE_OK;
+ }
+
+ return ret;
+ }
+
+ private boolean isDelegationNeeded(String caller, RangerUserInfo user) {
+ String userName = user.getName();
+ boolean needsDelegation = !caller.equals(userName);
+
+ if (!needsDelegation) {
+ // don't trust user-attributes/groups/roles if caller doesn't have delegation permission
+ needsDelegation = MapUtils.isNotEmpty(user.getAttributes()) || CollectionUtils.isNotEmpty(user.getGroups()) || CollectionUtils.isNotEmpty(user.getRoles());
+ }
+
+ return needsDelegation;
+ }
+
+ private boolean isDelegationNeeded(RangerAccessInfo access) {
+ RangerResourceInfo resource = access != null ? access.getResource() : null;
+
+ // delegation permission is needed when resource attributes are specified
+ return (resource != null && MapUtils.isNotEmpty(resource.getAttributes()));
+ }
+
+ private boolean isDelegationNeeded(List<RangerAccessInfo> accesses) {
+ if (accesses != null) {
+ for (RangerAccessInfo access : accesses) {
+ if (isDelegationNeeded(access)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private RangerPdpStats getRuntimeState(HttpServletRequest httpRequest) {
+ Object state = httpRequest.getServletContext().getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE);
+
+ if (state instanceof RangerPdpStats) {
+ return (RangerPdpStats) state;
+ } else {
+ Object fallback = servletContext != null ? servletContext.getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE) : null;
+
+ return (fallback instanceof RangerPdpStats) ? (RangerPdpStats) fallback : new RangerPdpStats();
+ }
+ }
+
+ private void recordRequestMetrics(Response ret, long startNanos, HttpServletRequest httpRequest) {
+ RangerPdpStats state = getRuntimeState(httpRequest);
+ int status = ret != null ? ret.getStatus() : 500;
+ long elapsed = System.nanoTime() - startNanos;
+
+ if (status >= 200 && status < 300) {
+ state.recordRequestSuccess(elapsed);
+ } else if (status == 401 || status == 403) { // UNAUTHORIZED or FORBIDDEN
+ state.recordAuthFailure(elapsed);
+ } else if (status == 400) {
+ state.recordRequestBadRequest(elapsed);
+ } else {
+ state.recordRequestError(elapsed);
+ }
+ }
+
+ private boolean isDelegationUserForService(String serviceName, String userName) {
+ final boolean ret;
+ Map<String, Set<String>> delegationUsersByService = this.delegationUsersByService;
+
+ if (delegationUsersByService.isEmpty()) {
+ ret = false;
+ } else if (StringUtils.isBlank(serviceName) || StringUtils.isBlank(userName)) {
+ ret = false;
+ } else {
+ Set<String> delegationUsers = delegationUsersByService.get(serviceName);
+
+ if (delegationUsers == null) {
+ delegationUsers = delegationUsersByService.get(RangerPdpConstants.WILDCARD_SERVICE_NAME);
+ }
+
+ ret = delegationUsers != null && delegationUsers.contains(userName);
+ }
+
+ LOG.debug("isDelegationUserForService(serviceName={}, userName={}): ret={}", serviceName, userName, ret);
+
+ return ret;
+ }
+
+ private void initializeDelegationUsers() {
+ Properties properties = config != null ? config.getAuthzProperties() : new Properties();
+
+ for (String key : properties.stringPropertyNames()) {
+ if (key.startsWith(PROP_PDP_SERVICE_PREFIX) && key.endsWith(PROP_SUFFIX_DELEGATION_USERS)) {
+ String serviceName = key.substring(PROP_PDP_SERVICE_PREFIX.length(), key.length() - PROP_SUFFIX_DELEGATION_USERS.length());
+
+ if (StringUtils.isBlank(serviceName)) {
+ continue;
+ }
+
+ Set<String> delegationUsers = Arrays.stream(StringUtils.defaultString(properties.getProperty(key)).split(","))
+ .map(String::trim)
+ .filter(StringUtils::isNotBlank)
+ .collect(Collectors.toSet());
+
+ if (!delegationUsers.isEmpty()) {
+ delegationUsersByService.put(serviceName, delegationUsers);
+
+ LOG.info("Delegation users for service '{}': {}", serviceName, delegationUsers);
+ }
+ }
+ }
+
+ if (delegationUsersByService.isEmpty()) {
+ LOG.warn("No delegation users configured");
+ } else {
+ Set<String> wildcardDelegationUsers = delegationUsersByService.get(WILDCARD_SERVICE_NAME);
+
+ // delegation users for WILDCARD_SERVICE_NAME are delegates for all services
+ if (wildcardDelegationUsers != null && !wildcardDelegationUsers.isEmpty()) {
+ delegationUsersByService.forEach((k, v) -> v.addAll(wildcardDelegationUsers));
+ }
+ }
+ }
+
+ private Response badRequest(RangerAuthzException e) {
+ return Response.status(BAD_REQUEST)
+ .entity(new ErrorResponse(BAD_REQUEST, e.getMessage()))
+ .build();
+ }
+
+ private Response serverError() {
+ return Response.serverError()
+ .entity(new ErrorResponse(INTERNAL_SERVER_ERROR, "Internal Server Error"))
+ .build();
+ }
+
+ private boolean isStatusOk(Response resp) {
+ return resp != null && resp.getStatus() == OK.getStatusCode();
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
new file mode 100644
index 000000000..60df86daf
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
@@ -0,0 +1,71 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.Properties;
+
+/**
+ * Authenticates requests by extracting the username from a trusted HTTP request header.
+ *
+ * <p>This handler is intended for deployments where a trusted reverse proxy / API gateway
+ * has already authenticated the caller and propagates the identity via a header
+ * (e.g., {@code X-Remote-User}). The header value is accepted as-is; no further
+ * validation is performed.
+ *
+ * <p><strong>Security note:</strong> Only enable this handler when the Ranger PDP server
+ * is reachable exclusively through the trusted proxy. Direct client access would allow
+ * unauthenticated identity spoofing.
+ */
+public class HttpHeaderAuthNHandler implements PdpAuthNHandler {
+ private static final Logger LOG = LoggerFactory.getLogger(HttpHeaderAuthNHandler.class);
+
+ public static final String AUTH_TYPE = "HEADER";
+
+ private String usernameHeader;
+
+ @Override
+ public void init(Properties config) {
+ usernameHeader = config.getProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Forwarded-User");
+
+ LOG.info("HttpHeaderAuthHandler initialized; username header={}", usernameHeader);
+ }
+
+ @Override
+ public Result authenticate(HttpServletRequest request, HttpServletResponse response) {
+ String userName = request.getHeader(usernameHeader);
+
+ LOG.debug("authenticate(): user={} (from header {})", userName, usernameHeader);
+
+ return StringUtils.isBlank(userName) ? Result.skip() : Result.authenticated(userName.trim(), AUTH_TYPE);
+ }
+
+ @Override
+ public String getChallengeHeader() {
+ return null;
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java
new file mode 100644
index 000000000..4e45c0031
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java
@@ -0,0 +1,105 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.ranger.authz.handler.RangerAuth;
+import org.apache.ranger.authz.handler.jwt.RangerDefaultJwtAuthHandler;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.Properties;
+
+/**
+ * Authenticates requests using a JWT bearer token.
+ *
+ * <p>Checks for the token in the {@code Authorization: Bearer <token>} header first,
+ * then in the configured JWT cookie. Delegates signature verification and expiry/audience
+ * checks to {@link RangerDefaultJwtAuthHandler} from the {@code ranger-authn} module.
+ *
+ * <p>Configuration keys (all prefixed with {@code ranger.pdp.authn.jwt.}):
+ * <ul>
+ * <li>{@code provider.url} – JWKS endpoint URL (optional if public key is set)
+ * <li>{@code public.key} – PEM-encoded public key (optional if provider URL is set)
+ * <li>{@code cookie.name} – JWT cookie name (default: {@code hadoop-jwt})
+ * <li>{@code audiences} – comma-separated list of accepted audiences (optional)
+ * </ul>
+ */
+public class JwtAuthNHandler implements PdpAuthNHandler {
+ private static final Logger LOG = LoggerFactory.getLogger(JwtAuthNHandler.class);
+
+ public static final String AUTH_TYPE = "JWT";
+
+ private RangerDefaultJwtAuthHandler delegate;
+
+ @Override
+ public void init(Properties config) throws Exception {
+ Properties jwtConfig = new Properties();
+
+ copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, jwtConfig, RangerDefaultJwtAuthHandler.KEY_PROVIDER_URL);
+ copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_PUBLIC_KEY);
+ copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_COOKIE_NAME);
+ copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_AUDIENCES);
+
+ delegate = new RangerDefaultJwtAuthHandler();
+
+ delegate.initialize(jwtConfig);
+
+ LOG.info("JwtAuthHandler initialized");
+ }
+
+ @Override
+ public Result authenticate(HttpServletRequest request, HttpServletResponse response) {
+ if (!RangerDefaultJwtAuthHandler.canAuthenticateRequest(request)) {
+ return Result.skip();
+ }
+
+ RangerAuth rangerAuth = delegate.authenticate(request);
+
+ if (rangerAuth != null && rangerAuth.isAuthenticated()) {
+ LOG.debug("authenticate(): user={}", rangerAuth.getUserName());
+
+ return Result.authenticated(rangerAuth.getUserName(), AUTH_TYPE);
+ }
+
+ LOG.warn("authenticate(): JWT validation failed");
+
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ response.addHeader("WWW-Authenticate", getChallengeHeader() + ", error=\"invalid_token\"");
+
+ return Result.challenge();
+ }
+
+ @Override
+ public String getChallengeHeader() {
+ return "Bearer realm=\"Ranger PDP\"";
+ }
+
+ private void copyIfPresent(Properties src, String srcKey, Properties dst, String dstKey) {
+ String val = src.getProperty(srcKey);
+
+ if (val != null && !val.isEmpty()) {
+ dst.setProperty(dstKey, val);
+ }
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java
new file mode 100644
index 000000000..e46e3e5d3
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java
@@ -0,0 +1,275 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Authenticates requests using Kerberos SPNEGO (HTTP Negotiate).
+ *
+ * <p>Uses the JDK's built-in GSSAPI/JGSS support – no external Kerberos library is required.
+ * The service principal and keytab must be configured via:
+ * <ul>
+ * <li>{@code ranger.pdp.authn.kerberos.spnego.principal} – e.g. {@code HTTP/host.example.com@REALM}
+ * <li>{@code ranger.pdp.authn.kerberos.spnego.keytab} – absolute path to the keytab file
+ * <li>{@code ranger.pdp.authn.kerberos.name.rules} – Hadoop-style name rules (default: {@code DEFAULT})
+ * </ul>
+ *
+ * <p>Authentication flow:
+ * <ol>
+ * <li>If no {@code Authorization: Negotiate} header is present the handler returns {@code SKIP}.
+ * <li>The SPNEGO token is extracted, validated via GSSAPI, and – if a response token is
+ * produced (mutual authentication) – written to {@code WWW-Authenticate: Negotiate <token>}.
+ * <li>On success the short-form principal name (strip {@literal @REALM} and host components)
+ * is returned as the authenticated user.
+ * <li>On failure a {@code 401 Negotiate} challenge is sent and {@code CHALLENGE} is returned.
+ * </ol>
+ */
+public class KerberosAuthNHandler implements PdpAuthNHandler {
+ private static final Logger LOG = LoggerFactory.getLogger(KerberosAuthNHandler.class);
+
+ public static final String AUTH_TYPE = "KERBEROS";
+
+ private static final String NEGOTIATE_PREFIX = "Negotiate ";
+ private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+ private static final String AUTHORIZATION = "Authorization";
+ private static final Oid SPNEGO_OID;
+ private static final Oid KRB5_OID;
+
+ static {
+ try {
+ SPNEGO_OID = new Oid("1.3.6.1.5.5.2");
+ KRB5_OID = new Oid("1.2.840.113554.1.2.2");
+ } catch (GSSException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ private Subject serviceSubject;
+ private GSSManager gssManager;
+ private GSSCredential serverCred;
+
+ @Override
+ public void init(Properties config) throws Exception {
+ String principal = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL);
+ String keytab = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB);
+ String nameRules = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, "DEFAULT");
+ String tokenValidity = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY);
+
+ int tokenLifetime = StringUtils.isBlank(tokenValidity) ? GSSCredential.INDEFINITE_LIFETIME : Integer.parseInt(tokenValidity);
+
+ if (StringUtils.isBlank(principal) || StringUtils.isBlank(keytab)) {
+ throw new IllegalArgumentException("Kerberos auth requires configurations " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL + " and " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB);
+ }
+
+ serviceSubject = loginWithKeytab(principal, keytab);
+
+ initializeKerberosNameRules(nameRules);
+
+ gssManager = GSSManager.getInstance();
+
+ GSSName serverName = gssManager.createName(principal, GSSName.NT_USER_NAME);
+
+ // Create acceptor credentials in the logged-in Subject to avoid fallback to JVM default keytab.
+ serverCred = Subject.doAs(serviceSubject, (PrivilegedExceptionAction<GSSCredential>) () ->
+ gssManager.createCredential(serverName, tokenLifetime, new Oid[] {SPNEGO_OID, KRB5_OID}, GSSCredential.ACCEPT_ONLY));
+
+ LOG.info("KerberosAuthNHandler initialized; principal={} (bound acceptor credential to configured principal)", principal);
+ }
+
+ @Override
+ public Result authenticate(HttpServletRequest request, HttpServletResponse response) {
+ String authHeader = request.getHeader(AUTHORIZATION);
+
+ if (authHeader == null || !authHeader.startsWith(NEGOTIATE_PREFIX)) {
+ return Result.skip();
+ }
+
+ byte[] inputToken = Base64.decodeBase64(authHeader.substring(NEGOTIATE_PREFIX.length()).trim());
+
+ try {
+ return Subject.doAs(serviceSubject, (PrivilegedExceptionAction<Result>) () -> validateSpnegoToken(inputToken, response));
+ } catch (Exception e) {
+ LOG.warn("authenticate(): SPNEGO validation error", e);
+
+ sendUnauthorized(response, null);
+
+ return Result.challenge();
+ }
+ }
+
+ @Override
+ public String getChallengeHeader() {
+ return NEGOTIATE_PREFIX.trim();
+ }
+
+ private Result validateSpnegoToken(byte[] inputToken, HttpServletResponse response) throws GSSException {
+ GSSContext gssCtx = gssManager.createContext(serverCred);
+
+ try {
+ byte[] outputToken = gssCtx.acceptSecContext(inputToken, 0, inputToken.length);
+
+ if (outputToken != null && outputToken.length > 0) {
+ response.addHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX + Base64.encodeBase64String(outputToken));
+ }
+
+ if (!gssCtx.isEstablished()) {
+ sendUnauthorized(response, outputToken);
+
+ return Result.challenge();
+ }
+
+ String principal = gssCtx.getSrcName().toString();
+ String userName = applyNameRules(principal);
+
+ LOG.debug("authenticate(): SPNEGO success, principal={}, user={}", principal, userName);
+
+ return Result.authenticated(userName, AUTH_TYPE);
+ } finally {
+ try {
+ gssCtx.dispose();
+ } catch (GSSException excp) {
+ LOG.debug("GSSContext.dispose() failed. Ignored", excp);
+ }
+ }
+ }
+
+ private void sendUnauthorized(HttpServletResponse response, byte[] outputToken) {
+ if (!response.isCommitted()) {
+ if (outputToken != null && outputToken.length > 0) {
+ response.setHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX + Base64.encodeBase64String(outputToken));
+ } else {
+ response.setHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX.trim());
+ }
+
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ }
+ }
+
+ /**
+ * Applies configured auth_to_local rules.
+ *
+ * <p>Uses Hadoop {@link KerberosName} with configured auth_to_local rules.
+ * Falls back to DEFAULT short-name mapping only if transformation fails.
+ */
+ private String applyNameRules(String principal) {
+ try {
+ String shortName = new KerberosName(principal).getShortName();
+
+ if (StringUtils.isNotBlank(shortName)) {
+ return shortName;
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed KerberosName transformation for principal '{}'; using DEFAULT short-name mapping", principal, e);
+ }
+
+ return defaultShortName(principal);
+ }
+
+ private void initializeKerberosNameRules(String configuredRules) {
+ String effectiveRules = StringUtils.defaultIfBlank(configuredRules, "DEFAULT").trim();
+
+ KerberosName.setRules(effectiveRules);
+
+ LOG.info("Initialized Kerberos name rules: {}='{}'", RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, effectiveRules);
+ }
+
+ private String defaultShortName(String principal) {
+ String name = principal;
+ int atSign = name.indexOf('@');
+
+ if (atSign >= 0) {
+ name = name.substring(0, atSign);
+ }
+
+ int slash = name.indexOf('/');
+
+ if (slash >= 0) {
+ name = name.substring(0, slash);
+ }
+
+ return name;
+ }
+
+ private Subject loginWithKeytab(String principal, String keytab) throws LoginException {
+ Configuration jaasConfig = new Configuration() {
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
+ Map<String, String> opts = new HashMap<>();
+
+ opts.put("useKeyTab", "true");
+ opts.put("storeKey", "true");
+ opts.put("doNotPrompt", "true");
+ opts.put("useTicketCache", "false");
+ opts.put("keyTab", keytab);
+ opts.put("principal", principal);
+ opts.put("refreshKrb5Config", "true");
+ opts.put("isInitiator", "false");
+
+ return new AppConfigurationEntry[] {
+ new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts)
+ };
+ }
+ };
+
+ LoginContext lc = new LoginContext("Ranger-PDP-Kerberos", null, noOpCallbackHandler(), jaasConfig);
+
+ lc.login();
+
+ return lc.getSubject();
+ }
+
+ private static CallbackHandler noOpCallbackHandler() {
+ return (Callback[] callbacks) -> {
+ for (Callback cb : callbacks) {
+ LOG.warn("Unexpected JAAS callback: {}", cb.getClass().getName());
+
+ throw new UnsupportedCallbackException(cb);
+ }
+ };
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java
new file mode 100644
index 000000000..4bb11aea5
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java
@@ -0,0 +1,111 @@
+/*
+ * 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.pdp.security;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Contract for PDP authentication handlers.
+ *
+ * <p>Each handler is responsible for a single credential type (Kerberos, JWT, HTTP Header).
+ * The {@link RangerPdpAuthNFilter} tries handlers in configured order and uses the first one
+ * that returns {@link Result.Status#AUTHENTICATED}.
+ */
+public interface PdpAuthNHandler {
+ /**
+ * Initializes the handler with filter init parameters.
+ *
+ * @param config filter init parameters
+ * @throws Exception on initialization failure
+ */
+ void init(Properties config) throws Exception;
+
+ /**
+ * Attempts to authenticate the incoming request.
+ *
+ * <p>Handlers must follow this contract:
+ * <ul>
+ * <li>{@link Result.Status#AUTHENTICATED} - credentials were present and valid.
+ * The handler may write {@code WWW-Authenticate} response headers (e.g., for SPNEGO
+ * mutual authentication) but must NOT commit the response.
+ * <li>{@link Result.Status#CHALLENGE} - credentials were present but invalid, or a
+ * multi-round negotiation step was sent. The handler has already written a
+ * {@code 401} response with an appropriate challenge header; the filter must not
+ * continue processing.
+ * <li>{@link Result.Status#SKIP} - this handler cannot process the request (the expected
+ * credential type is absent). The filter should try the next handler.
+ * </ul>
+ *
+ * @param request the HTTP request
+ * @param response the HTTP response (may be written to for challenges)
+ * @return authentication result
+ * @throws IOException on I/O error
+ */
+ Result authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException;
+
+ /**
+ * Returns the {@code WWW-Authenticate} header value to include in a terminal 401
+ * response when no handler can process the request.
+ * Returns {@code null} if this handler does not contribute a challenge header.
+ */
+ String getChallengeHeader();
+
+ class Result {
+ public enum Status { AUTHENTICATED, CHALLENGE, SKIP }
+
+ private final Status status;
+ private final String userName;
+ private final String authType;
+
+ private Result(Status status, String userName, String authType) {
+ this.status = status;
+ this.userName = userName;
+ this.authType = authType;
+ }
+
+ public static Result authenticated(String userName, String authType) {
+ return new Result(Status.AUTHENTICATED, userName, authType);
+ }
+
+ public static Result challenge() {
+ return new Result(Status.CHALLENGE, null, null);
+ }
+
+ public static Result skip() {
+ return new Result(Status.SKIP, null, null);
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public String getAuthType() {
+ return authType;
+ }
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java
new file mode 100644
index 000000000..32d04ef0a
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.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.ranger.pdp.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_AUTHN_TYPES;
+
+/**
+ * Servlet filter that enforces authentication for all PDP REST endpoints.
+ *
+ * <p>Handlers are configured via the {@code ranger.pdp.authn.types} filter init parameter
+ * (comma-separated list of {@code header}, {@code jwt}, {@code kerberos}). Handlers are
+ * tried in the listed order; the first successful match wins.
+ *
+ * <p>On success the authenticated username is stored in the request attribute
+ * {@link RangerPdpConstants#ATTR_AUTHENTICATED_USER} so that REST resources can read it.
+ *
+ * <p>If all handlers return {@code SKIP} (no recognisable credentials found), the filter
+ * sends a {@code 401} response with {@code WWW-Authenticate} headers for every
+ * configured handler that provides a challenge.
+ */
+public class RangerPdpAuthNFilter implements Filter {
+ private static final Logger LOG = LoggerFactory.getLogger(RangerPdpAuthNFilter.class);
+
+ private final List<PdpAuthNHandler> handlers = new ArrayList<>();
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ Properties config = toProperties(filterConfig);
+ String authnTypes = filterConfig.getInitParameter(PROP_AUTHN_TYPES);
+
+ if (StringUtils.isNotBlank(authnTypes)) {
+ for (String authnType : authnTypes.split(",")) {
+ PdpAuthNHandler handler = createHandler(authnType.trim().toLowerCase(), filterConfig);
+
+ if (handler == null) {
+ continue;
+ }
+
+ try {
+ handler.init(config);
+
+ handlers.add(handler);
+
+ LOG.info("{}: successfully registered authentication handler", authnType);
+ } catch (Exception excp) {
+ LOG.error("{}: failed to initialize authentication handler. Handler disabled", authnType, excp);
+ }
+ }
+ }
+
+ if (handlers.isEmpty()) {
+ throw new ServletException("No valid authentication handlers configured");
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest httpReq = (HttpServletRequest) request;
+ HttpServletResponse httpResp = (HttpServletResponse) response;
+
+ for (PdpAuthNHandler handler : handlers) {
+ PdpAuthNHandler.Result result = handler.authenticate(httpReq, httpResp);
+
+ switch (result.getStatus()) {
+ case AUTHENTICATED:
+ httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER, result.getUserName());
+ httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHN_TYPE, result.getAuthType());
+
+ LOG.debug("doFilter(): authenticated user={}, type={}", result.getUserName(), result.getAuthType());
+
+ chain.doFilter(request, response);
+ return;
+
+ case CHALLENGE:
+ // handler has already written the 401; stop processing
+ return;
+
+ case SKIP:
+ default:
+ // try the next handler
+ break;
+ }
+ }
+
+ // No handler could authenticate the request; send a 401 with all challenge headers
+ LOG.debug("doFilter(): no handler authenticated request from {}", httpReq.getRemoteAddr());
+
+ sendUnauthenticated(httpResp);
+ }
+
+ @Override
+ public void destroy() {
+ handlers.clear();
+ }
+
+ private void sendUnauthenticated(HttpServletResponse response) throws IOException {
+ for (PdpAuthNHandler handler : handlers) {
+ String challenge = handler.getChallengeHeader();
+
+ if (StringUtils.isNotBlank(challenge)) {
+ response.addHeader("WWW-Authenticate", challenge);
+ }
+ }
+
+ response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
+ response.setContentType(MediaType.APPLICATION_JSON);
+ response.getWriter().write("{\"code\":\"UNAUTHORIZED\",\"message\":\"Authentication required\"}");
+ }
+
+ private PdpAuthNHandler createHandler(String type, FilterConfig filterConfig) {
+ final PdpAuthNHandler ret;
+
+ switch (type) {
+ case "header":
+ ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED) ? new HttpHeaderAuthNHandler() : null;
+ break;
+ case "jwt":
+ ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_JWT_ENABLED) ? new JwtAuthNHandler() : null;
+ break;
+ case "kerberos":
+ ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED) ? new KerberosAuthNHandler() : null;
+ break;
+ default:
+ ret = null;
+ break;
+ }
+
+ if (ret == null) {
+ LOG.warn("{}: authentication type unknown or disabled. Ignored", type);
+ }
+
+ return ret;
+ }
+
+ private Properties toProperties(FilterConfig filterConfig) {
+ Properties props = new Properties();
+ java.util.Enumeration<String> names = filterConfig.getInitParameterNames();
+
+ while (names.hasMoreElements()) {
+ String name = names.nextElement();
+
+ props.setProperty(name, filterConfig.getInitParameter(name));
+ }
+
+ return props;
+ }
+
+ private boolean getBoolean(FilterConfig config, String name) {
+ return Boolean.parseBoolean(config.getInitParameter(name));
+ }
+}
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java
new file mode 100644
index 000000000..8d9ccc16f
--- /dev/null
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java
@@ -0,0 +1,73 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.MDC;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.UUID;
+
+public class RangerPdpRequestContextFilter implements Filter {
+ public static final String REQ_HEADER_REQUEST_ID = "X-Request-Id";
+ public static final String RES_HEADER_REQUEST_ID = "X-Request-Id";
+ public static final String MDC_REQUEST_ID = "requestId";
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ // no-op
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ HttpServletResponse res = (HttpServletResponse) response;
+
+ String requestId = req.getHeader(REQ_HEADER_REQUEST_ID);
+
+ if (StringUtils.isBlank(requestId)) {
+ requestId = UUID.randomUUID().toString();
+ }
+
+ MDC.put(MDC_REQUEST_ID, requestId);
+ req.setAttribute(MDC_REQUEST_ID, requestId);
+ res.setHeader(RES_HEADER_REQUEST_ID, requestId);
+
+ try {
+ chain.doFilter(request, response);
+ } finally {
+ MDC.remove(MDC_REQUEST_ID);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ // no-op
+ }
+}
diff --git a/pdp/src/main/resources/ranger-pdp-default.xml b/pdp/src/main/resources/ranger-pdp-default.xml
new file mode 100644
index 000000000..ddf2ff934
--- /dev/null
+++ b/pdp/src/main/resources/ranger-pdp-default.xml
@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+ 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.
+-->
+<configuration>
+ <property>
+ <name>ranger.pdp.port</name>
+ <value>6500</value>
+ <description>Port the PDP server listens on.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.log.dir</name>
+ <value>/var/log/ranger/pdp</value>
+ <description>Directory for PDP server log files.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.service.*.delegation.users</name>
+ <value/>
+ <description>Comma-separated users, allowed to call on behalf of other users in all services</description>
+ </property>
+
+ <!-- SSL/TLS -->
+ <property>
+ <name>ranger.pdp.ssl.enabled</name>
+ <value>false</value>
+ <description>Set to true to enable HTTPS.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.keystore.file</name>
+ <value/>
+ <description>Path to the keystore file (required when SSL is enabled).</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.keystore.password</name>
+ <value/>
+ <description>Keystore password.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.keystore.type</name>
+ <value>JKS</value>
+ <description>Keystore type (JKS, PKCS12).</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.truststore.enabled</name>
+ <value>false</value>
+ <description>Set to true to require client certificate authentication.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.truststore.file</name>
+ <value/>
+ <description>Path to the truststore file.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.truststore.password</name>
+ <value/>
+ <description>Truststore password.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.ssl.truststore.type</name>
+ <value>JKS</value>
+ <description>Truststore type (JKS, PKCS12).</description>
+ </property>
+
+ <!-- HTTP/2 -->
+ <property>
+ <name>ranger.pdp.http2.enabled</name>
+ <value>true</value>
+ <description>
+ Enable HTTP/2 via upgrade on the connector.
+ Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside HTTP/1.1.
+ </description>
+ </property>
+
+ <!-- Connector concurrency / queue limits -->
+ <property>
+ <name>ranger.pdp.http.connector.maxThreads</name>
+ <value>200</value>
+ <description>Maximum number of worker threads handling simultaneous requests.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.minSpareThreads</name>
+ <value>20</value>
+ <description>Minimum number of spare worker threads kept ready.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.acceptCount</name>
+ <value>100</value>
+ <description>Queued connection backlog when all worker threads are busy.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.http.connector.maxConnections</name>
+ <value>10000</value>
+ <description>Maximum concurrent TCP connections accepted by the connector.</description>
+ </property>
+
+ <!-- Authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.types</name>
+ <value>header,jwt,kerberos</value>
+ <description>
+ Comma-separated list of authentication methods for incoming REST requests,
+ tried in listed order. Supported values: header, jwt, kerberos.
+ </description>
+ </property>
+
+ <!-- HTTP Header authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.header.enabled</name>
+ <value>false</value>
+ <description>
+ Enable trusted HTTP header authentication. Use only behind a trusted proxy.
+ </description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.header.username</name>
+ <value>X-Forwarded-User</value>
+ <description>HTTP header name from which the authenticated username is read.</description>
+ </property>
+
+ <!-- JWT bearer token authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.jwt.enabled</name>
+ <value>false</value>
+ <description>Enable JWT authentication.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.provider.url</name>
+ <value/>
+ <description>URL of the JWT provider (used to fetch the public key).</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.public.key</name>
+ <value/>
+ <description>PEM-encoded public key for verifying JWT signatures.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.cookie.name</name>
+ <value>hadoop-jwt</value>
+ <description>Cookie name from which a JWT bearer token may be read.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.jwt.audiences</name>
+ <value/>
+ <description>Comma-separated list of accepted JWT audiences. Empty means any audience is accepted.</description>
+ </property>
+
+ <!-- Kerberos / SPNEGO authentication for incoming REST requests -->
+ <property>
+ <name>ranger.pdp.authn.kerberos.enabled</name>
+ <value>false</value>
+ <description>Enable Kerberos authentication.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.spnego.principal</name>
+ <value/>
+ <description>Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.spnego.keytab</name>
+ <value/>
+ <description>Path to the keytab file for the SPNEGO service principal.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.token.valid.seconds</name>
+ <value>3600</value>
+ <description>Validity period (seconds) for Kerberos authentication tokens.</description>
+ </property>
+
+ <property>
+ <name>ranger.pdp.authn.kerberos.name.rules</name>
+ <value>DEFAULT</value>
+ <description>Rules for mapping Kerberos principal names to short usernames.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.app.type</name>
+ <value>ranger-pdp</value>
+ <description>Application type reported in audit records.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.init.services</name>
+ <value/>
+ <description>
+ Comma-separated list of Ranger service names to eagerly initialize at startup.
+ Leave empty for lazy initialization on first access.
+ Example: dev_hive,dev_hdfs
+ </description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.source.impl</name>
+ <value>org.apache.ranger.admin.client.RangerAdminRESTClient</value>
+ <description>Policy source implementation. Use RangerAdminRESTClient to download policies live from Ranger Admin.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.url</name>
+ <value>http://localhost:6080</value>
+ <description>
+ URL of the Ranger Admin server. Comma-separated for HA.
+ Example: http://ranger-admin-1:6080,http://ranger-admin-2:6080
+ </description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.ssl.config.file</name>
+ <value/>
+ <description>
+ Path to the XML file containing SSL keystore/truststore settings for the
+ connection to Ranger Admin (required when the Admin URL uses https://).
+ The file uses the standard Ranger policymgr SSL property names:
+ xasecure.policymgr.clientssl.truststore
+ xasecure.policymgr.clientssl.truststore.credential.file
+ xasecure.policymgr.clientssl.keystore (for mutual TLS)
+ xasecure.policymgr.clientssl.keystore.credential.file
+ </description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.client.connection.timeoutMs</name>
+ <value>120000</value>
+ <description>HTTP connection timeout (ms) for calls to Ranger Admin.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.client.read.timeoutMs</name>
+ <value>30000</value>
+ <description>HTTP read timeout (ms) for calls to Ranger Admin.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.pollIntervalMs</name>
+ <value>30000</value>
+ <description>How often (ms) to poll Ranger Admin for updated policies and roles.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.cache.dir</name>
+ <value>/var/ranger/cache/pdp</value>
+ <description>
+ Directory for local JSON cache files (policies, tags, roles, userstore, GDS).
+ The PDP can serve authorization requests from cache while Ranger Admin is unavailable.
+ </description>
+ </property>
+
+ <!-- Basic Auth authentication for connecting to Ranger Admin -->
+ <property>
+ <name>ranger.authz.default.policy.rest.client.username</name>
+ <value>admin</value>
+ <description>Username for connecting to Ranger Admin (basic auth).</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.policy.rest.client.password</name>
+ <value>admin</value>
+ <description>Password for connecting to Ranger Admin (basic auth).</description>
+ </property>
+
+ <!-- Kerberos authentication for connecting to Ranger Admin-->
+ <property>
+ <name>ranger.authz.default.ugi.initialize</name>
+ <value>false</value>
+ <description>Set to true to enable Kerberos login for the Ranger Admin connection.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.ugi.login.type</name>
+ <value/>
+ <description>Kerberos login type: keytab or jaas.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.ugi.keytab.principal</name>
+ <value/>
+ <description>Kerberos principal for keytab-based login.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.ugi.keytab.file</name>
+ <value/>
+ <description>Path to the keytab file for keytab-based login.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.ugi.jaas.appconfig</name>
+ <value/>
+ <description>JAAS application configuration name for jaas-based login.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.default.use.rangerGroups</name>
+ <value>false</value>
+ <description>
+ When true, group membership is resolved from Ranger's internal userstore
+ (downloaded from Ranger Admin) rather than the local OS.
+ </description>
+ </property>
+
+ <!-- Audit -->
+ <property>
+ <name>ranger.authz.audit.is.enabled</name>
+ <value>true</value>
+ <description>Master switch for audit logging.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.solr</name>
+ <value>false</value>
+ <description>Enable Solr as an audit destination.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.solr.urls</name>
+ <value/>
+ <description>Solr URL for audit logging.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.hdfs</name>
+ <value>false</value>
+ <description>Enable HDFS as an audit destination.</description>
+ </property>
+
+ <property>
+ <name>ranger.authz.audit.destination.hdfs.dir</name>
+ <value/>
+ <description>HDFS directory path for audit log files.</description>
+ </property>
+</configuration>
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java
new file mode 100644
index 000000000..5ed3cadd3
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.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.ranger.pdp;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RangerPdpStatsTest {
+ @Test
+ public void testRequestCountersAndAverageLatency() {
+ RangerPdpStats stats = new RangerPdpStats();
+
+ stats.recordRequestSuccess(5_000_000L); // 5 ms
+ stats.recordRequestBadRequest(15_000_000L); // 15 ms
+ stats.recordRequestError(10_000_000L); // 10 ms
+ stats.recordAuthFailure(10_000_000L);
+
+ assertEquals(4L, stats.getTotalRequests());
+ assertEquals(1L, stats.getTotalAuthzSuccess());
+ assertEquals(1L, stats.getTotalAuthzBadRequest());
+ assertEquals(1L, stats.getTotalAuthzErrors());
+ assertEquals(1L, stats.getTotalAuthFailures());
+ assertEquals(40_000_000L, stats.getTotalLatencyNanos());
+ assertEquals(10L, stats.getAverageLatencyMs());
+ }
+
+ @Test
+ public void testNegativeLatencyIsClampedToZero() {
+ RangerPdpStats stats = new RangerPdpStats();
+
+ stats.recordRequestSuccess(-100L);
+
+ assertEquals(0L, stats.getTotalLatencyNanos());
+ assertEquals(0L, stats.getAverageLatencyMs());
+ }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java
new file mode 100644
index 000000000..3b8f16c74
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.pdp;
+
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RangerPdpStatusServletTest {
+ @AfterEach
+ public void clearOverrides() {
+ System.clearProperty(RangerPdpConstants.PROP_AUTHZ_POLICY_CACHE_DIR);
+ }
+
+ @Test
+ public void testMetricsEndpointRendersCounters() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+
+ stats.recordRequestSuccess(5_000_000L);
+ stats.recordRequestError(5_000_000L);
+ stats.recordAuthFailure(5_000_000L);
+
+ RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(stats, RangerPdpStatusServlet.Mode.METRICS);
+ HttpServletRequest req = proxy(HttpServletRequest.class, (proxy, method, args) -> null);
+ ResponseCapture capture = new ResponseCapture();
+ HttpServletResponse resp = capture.responseProxy();
+
+ servlet.doGet(req, resp);
+
+ assertEquals(HttpServletResponse.SC_OK, capture.status);
+ assertTrue(capture.body.toString().contains("ranger_pdp_requests_total 3"));
+ assertTrue(capture.body.toString().contains("ranger_pdp_auth_failures_total 1"));
+ }
+
+ @Test
+ public void testLoadedServicesCount() throws Exception {
+ RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(new RangerPdpStats(), RangerPdpStatusServlet.Mode.READY);
+ HttpServletRequest req = proxy(HttpServletRequest.class, (proxy, method, args) -> null);
+ Method method = RangerPdpStatusServlet.class.getDeclaredMethod("getLoadedServicesCount", HttpServletRequest.class);
+
+ method.setAccessible(true);
+
+ Integer servicesCount = (Integer) method.invoke(servlet, req);
+
+ assertTrue(servicesCount >= 0L);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> T proxy(Class<T> iface, InvocationHandler handler) {
+ return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class<?>[] {iface}, handler);
+ }
+
+ private static final class ResponseCapture {
+ private int status;
+ private final StringWriter body = new StringWriter();
+ private final Map<String, String> headers = new HashMap<>();
+
+ private HttpServletResponse responseProxy() {
+ return proxy(HttpServletResponse.class, (proxy, method, args) -> {
+ switch (method.getName()) {
+ case "setStatus":
+ status = (Integer) args[0];
+ return null;
+ case "setContentType":
+ headers.put("Content-Type", (String) args[0]);
+ return null;
+ case "getWriter":
+ return new PrintWriter(body, true);
+ case "setHeader":
+ headers.put((String) args[0], (String) args[1]);
+ return null;
+ default:
+ return null;
+ }
+ });
+ }
+ }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java b/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java
new file mode 100644
index 000000000..79119171b
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.pdp.config;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RangerPdpConfigTest {
+ @AfterEach
+ public void clearSystemOverrides() {
+ System.clearProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME);
+ System.clearProperty(RangerPdpConstants.PROP_PORT);
+ }
+
+ @Test
+ public void testHeaderUserNameCanBeOverriddenBySystemProperty() {
+ System.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Test-User");
+
+ RangerPdpConfig config = new RangerPdpConfig();
+
+ assertEquals("X-Test-User", config.getHeaderAuthnUsername());
+ }
+
+ @Test
+ public void testInvalidPortFallsBackToDefault() {
+ System.setProperty(RangerPdpConstants.PROP_PORT, "not-a-number");
+
+ RangerPdpConfig config = new RangerPdpConfig();
+
+ assertEquals(6500, config.getPort());
+ }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java b/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java
new file mode 100644
index 000000000..1221af3bb
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java
@@ -0,0 +1,360 @@
+/*
+ * 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.pdp.rest;
+
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.authz.api.RangerAuthzApiErrorCode;
+import org.apache.ranger.authz.api.RangerAuthzException;
+import org.apache.ranger.authz.model.RangerAccessContext;
+import org.apache.ranger.authz.model.RangerAuthzRequest;
+import org.apache.ranger.authz.model.RangerAuthzResult;
+import org.apache.ranger.authz.model.RangerMultiAuthzRequest;
+import org.apache.ranger.authz.model.RangerMultiAuthzResult;
+import org.apache.ranger.authz.model.RangerResourcePermissions;
+import org.apache.ranger.authz.model.RangerResourcePermissionsRequest;
+import org.apache.ranger.authz.model.RangerUserInfo;
+import org.apache.ranger.pdp.RangerPdpStats;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Response;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_PDP_SERVICE_PREFIX;
+import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_SUFFIX_DELEGATION_USERS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class RangerPdpRESTTest {
+ private static final String CALLER_DELEGATION_ALLOWED = "alice";
+ private static final String CALLER_DELEGATION_NOT_ALLOWED = "bob";
+ private static final RangerUserInfo USER_USER1 = new RangerUserInfo("user1");
+ private static final RangerAccessContext CONTEXT_SERVICE_SVC1 = new RangerAccessContext("hive", "svc1");
+
+ @Test
+ public void testAuthorizeReturnsUnauthorizedWhenCallerMissing() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer();
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerAuthzRequest request = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.authorize(request, httpRequest(null, stats));
+
+ assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthFailures());
+ }
+
+ @Test
+ public void testAuthorizeReturnsForbiddenWhenCallerNotAllowed() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer();
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerAuthzRequest request = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.authorize(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats));
+
+ assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthFailures());
+ }
+
+ @Test
+ public void testAuthorizeReturnsOkAndRecordsSuccess() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer(new RangerAuthzResult("req-1", RangerAuthzResult.AccessDecision.ALLOW));
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerAuthzRequest request = new RangerAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.authorize(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalRequests());
+ assertEquals(1L, stats.getTotalAuthzSuccess());
+ }
+
+ @Test
+ public void testAuthorizeReturnsBadRequestOnAuthzException() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING));
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerAuthzRequest request = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.authorize(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthzBadRequest());
+ }
+
+ @Test
+ public void testAuthorizeMultiReturnsUnauthorizedWhenCallerMissing() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer();
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerMultiAuthzRequest request = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.authorizeMulti(request, httpRequest(null, stats));
+
+ assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthFailures());
+ }
+
+ @Test
+ public void testAuthorizeMultiReturnsForbiddenWhenCallerNotAllowed() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer();
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerMultiAuthzRequest request = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats));
+
+ assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthFailures());
+ }
+
+ @Test
+ public void testAuthorizeMultiReturnsOkAndRecordsSuccess() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer(new RangerMultiAuthzResult("req-1", RangerAuthzResult.AccessDecision.ALLOW));
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerMultiAuthzRequest request = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalRequests());
+ assertEquals(1L, stats.getTotalAuthzSuccess());
+ }
+
+ @Test
+ public void testAuthorizeMultiReturnsBadRequestOnAuthzException() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING));
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerMultiAuthzRequest request = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthzBadRequest());
+ }
+
+ @Test
+ public void testGetResourcePermissionsReturnsUnauthorizedWhenCallerMissing() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer();
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.getResourcePermissions(request, httpRequest(null, stats));
+
+ assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthFailures());
+ }
+
+ @Test
+ public void testGetResourcePermissionsReturnsForbiddenWhenCallerNotAllowed() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer();
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats));
+
+ assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthFailures());
+ }
+
+ @Test
+ public void testGetResourcePermissionsReturnsOkAndRecordsSuccess() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer(new RangerResourcePermissions());
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+ assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalRequests());
+ assertEquals(1L, stats.getTotalAuthzSuccess());
+ }
+
+ @Test
+ public void testGetResourcePermissionsReturnsBadRequestOnAuthzException() throws Exception {
+ RangerPdpStats stats = new RangerPdpStats();
+ TestAuthorizer authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING));
+ RangerPdpREST rest = createRest(authorizer, stats);
+ RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1);
+ Response response = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats));
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+ assertEquals(1L, stats.getTotalAuthzBadRequest());
+ }
+
+ private static RangerPdpREST createRest(TestAuthorizer authorizer, RangerPdpStats stats) throws Exception {
+ RangerPdpREST rest = new RangerPdpREST();
+
+ setField(rest, "authorizer", authorizer);
+ setField(rest, "config", new TestConfig(defaultConfig()));
+ setField(rest, "servletContext", servletContextWithStats(stats));
+ rest.initialize();
+
+ return rest;
+ }
+
+ private static HttpServletRequest httpRequest(String user, RangerPdpStats stats) {
+ Map<String, Object> attrs = new HashMap<>();
+
+ if (user != null) {
+ attrs.put(RangerPdpConstants.ATTR_AUTHENTICATED_USER, user);
+ }
+
+ ServletContext servletContext = servletContextWithStats(stats);
+
+ return (HttpServletRequest) Proxy.newProxyInstance(
+ HttpServletRequest.class.getClassLoader(),
+ new Class<?>[] {HttpServletRequest.class},
+ (proxy, method, args) -> {
+ switch (method.getName()) {
+ case "getAttribute":
+ return attrs.get(args[0]);
+ case "setAttribute":
+ attrs.put((String) args[0], args[1]);
+ return null;
+ case "getServletContext":
+ return servletContext;
+ default:
+ return null;
+ }
+ });
+ }
+
+ private static ServletContext servletContextWithStats(RangerPdpStats stats) {
+ Map<String, Object> attrs = new HashMap<>();
+
+ attrs.put(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE, stats);
+
+ return (ServletContext) Proxy.newProxyInstance(
+ ServletContext.class.getClassLoader(),
+ new Class<?>[] {ServletContext.class},
+ (proxy, method, args) -> {
+ if ("getAttribute".equals(method.getName())) {
+ return attrs.get(args[0]);
+ } else if ("setAttribute".equals(method.getName())) {
+ attrs.put((String) args[0], args[1]);
+ return null;
+ }
+ return null;
+ });
+ }
+
+ private static void setField(Object target, String fieldName, Object value) throws Exception {
+ Field field = target.getClass().getDeclaredField(fieldName);
+
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+
+ private static Properties defaultConfig() {
+ Properties ret = new Properties();
+
+ ret.setProperty(PROP_PDP_SERVICE_PREFIX + "svc1" + PROP_SUFFIX_DELEGATION_USERS, CALLER_DELEGATION_ALLOWED);
+
+ return ret;
+ }
+
+ private static class TestConfig extends RangerPdpConfig {
+ private final Properties props;
+
+ TestConfig(Properties props) {
+ this.props = props;
+ }
+
+ @Override
+ public Properties getAuthzProperties() {
+ return new Properties(props);
+ }
+ }
+
+ private static class TestAuthorizer extends RangerAuthorizer {
+ private final RangerAuthzResult authzResult;
+ private final RangerMultiAuthzResult multiAuthzResult;
+ private final RangerResourcePermissions permissions;
+ private final RangerAuthzException authzException;
+
+ TestAuthorizer() {
+ this(null, null, null, null);
+ }
+
+ TestAuthorizer(RangerAuthzResult result) {
+ this(result, null, null, null);
+ }
+
+ TestAuthorizer(RangerMultiAuthzResult result) {
+ this(null, result, null, null);
+ }
+
+ TestAuthorizer(RangerResourcePermissions permissions) {
+ this(null, null, permissions, null);
+ }
+
+ TestAuthorizer(RangerAuthzException excp) {
+ this(null, null, null, excp);
+ }
+
+ TestAuthorizer(RangerAuthzResult result, RangerMultiAuthzResult multiResult, RangerResourcePermissions permissions, RangerAuthzException excp) {
+ super(new Properties());
+
+ this.authzResult = result;
+ this.multiAuthzResult = multiResult;
+ this.permissions = permissions;
+ this.authzException = excp;
+ }
+
+ @Override
+ public void init() {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public RangerAuthzResult authorize(RangerAuthzRequest request) throws RangerAuthzException {
+ if (authzException != null) {
+ throw authzException;
+ }
+
+ return authzResult != null ? authzResult : new RangerAuthzResult();
+ }
+
+ @Override
+ public RangerMultiAuthzResult authorize(RangerMultiAuthzRequest request) throws RangerAuthzException {
+ if (authzException != null) {
+ throw authzException;
+ }
+
+ return multiAuthzResult != null ? multiAuthzResult : new RangerMultiAuthzResult();
+ }
+
+ @Override
+ public RangerResourcePermissions getResourcePermissions(RangerResourcePermissionsRequest request) throws RangerAuthzException {
+ if (authzException != null) {
+ throw authzException;
+ }
+
+ return permissions != null ? permissions : new RangerResourcePermissions();
+ }
+ }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
new file mode 100644
index 000000000..c497be908
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.pdp.security;
+
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.http.HttpServletRequest;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class HttpHeaderAuthNHandlerTest {
+ @Test
+ public void testAuthenticate_usesDefaultHeaderName() {
+ HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+ Properties config = new Properties();
+
+ handler.init(config);
+
+ HttpServletRequest request = requestWithHeader("X-Forwarded-User", "alice");
+ PdpAuthNHandler.Result result = handler.authenticate(request, null);
+
+ assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus());
+ assertEquals("alice", result.getUserName());
+ assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE, result.getAuthType());
+ }
+
+ @Test
+ public void testAuthenticate_usesConfiguredHeaderName() {
+ HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
+ Properties config = new Properties();
+
+ config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Authenticated-User");
+
+ handler.init(config);
+
+ HttpServletRequest request = requestWithHeader("X-Authenticated-User", "bob");
+ PdpAuthNHandler.Result result = handler.authenticate(request, null);
+
+ assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus());
+ assertEquals("bob", result.getUserName());
+ }
+
+ private static HttpServletRequest requestWithHeader(String expectedHeader, String headerValue) {
+ InvocationHandler invocationHandler = (proxy, method, args) -> {
+ String methodName = method.getName();
+
+ if ("getHeader".equals(methodName)) {
+ return expectedHeader.equals(args[0]) ? headerValue : null;
+ } else if ("getHeaders".equals(methodName) || "getHeaderNames".equals(methodName)) {
+ return java.util.Collections.emptyEnumeration();
+ } else if ("getMethod".equals(methodName)) {
+ return "POST";
+ } else if ("toString".equals(methodName)) {
+ return "HttpServletRequest(test)";
+ }
+
+ return null;
+ };
+
+ return (HttpServletRequest) Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(), new Class<?>[] {HttpServletRequest.class}, invocationHandler);
+ }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java
new file mode 100644
index 000000000..891cd2bb8
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class KerberosAuthHandlerTest {
+ @AfterEach
+ public void restoreDefaultRules() {
+ KerberosName.setRules("DEFAULT");
+ }
+
+ @Test
+ public void testApplyNameRules_usesConfiguredRules() throws Exception {
+ KerberosAuthNHandler handler = new KerberosAuthNHandler();
+
+ invokePrivateVoid(handler, "initializeKerberosNameRules", new Class<?>[] {String.class}, "DEFAULT");
+
+ String shortName = (String) invokePrivate(handler, "applyNameRules", new Class<?>[] {String.class}, "alice@EXAMPLE.COM");
+
+ assertEquals("alice", shortName);
+ }
+
+ @Test
+ public void testApplyNameRules_fallsBackForInvalidPrincipal() throws Exception {
+ KerberosAuthNHandler handler = new KerberosAuthNHandler();
+
+ invokePrivateVoid(handler, "initializeKerberosNameRules", new Class<?>[] {String.class}, "DEFAULT");
+
+ String shortName = (String) invokePrivate(handler, "applyNameRules", new Class<?>[] {String.class}, "svc/host");
+
+ assertEquals("svc", shortName);
+ }
+
+ private static void invokePrivateVoid(Object target, String methodName, Class<?>[] paramTypes, Object... args) throws Exception {
+ invokePrivate(target, methodName, paramTypes, args);
+ }
+
+ private static Object invokePrivate(Object target, String methodName, Class<?>[] paramTypes, Object... args) throws Exception {
+ Method method = target.getClass().getDeclaredMethod(methodName, paramTypes);
+
+ method.setAccessible(true);
+
+ return method.invoke(target, args);
+ }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java
new file mode 100644
index 000000000..0d0e183c5
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.pdp.security;
+
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.junit.jupiter.api.Test;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Enumeration;
+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.assertThrows;
+
+public class RangerPdpAuthNFilterTest {
+ @Test
+ public void testInit_skipsHeaderHandlerWhenDisabled() {
+ RangerPdpAuthNFilter filter = new RangerPdpAuthNFilter();
+ Map<String, String> params = new HashMap<>();
+
+ params.put(RangerPdpConstants.PROP_AUTHN_TYPES, "header");
+ params.put(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, "false");
+
+ assertThrows(ServletException.class, () -> filter.init(new TestFilterConfig(params)));
+ }
+
+ @Test
+ public void testInit_registersHeaderHandlerWhenEnabled() throws Exception {
+ RangerPdpAuthNFilter filter = new RangerPdpAuthNFilter();
+ Map<String, String> params = new HashMap<>();
+
+ params.put(RangerPdpConstants.PROP_AUTHN_TYPES, "header");
+ params.put(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, "true");
+
+ filter.init(new TestFilterConfig(params));
+
+ Field handlersField = RangerPdpAuthNFilter.class.getDeclaredField("handlers");
+
+ handlersField.setAccessible(true);
+
+ @SuppressWarnings("unchecked")
+ List<PdpAuthNHandler> handlers = (List<PdpAuthNHandler>) handlersField.get(filter);
+
+ assertEquals(1, handlers.size());
+ assertEquals(HttpHeaderAuthNHandler.class, handlers.get(0).getClass());
+ }
+
+ private static final class TestFilterConfig implements FilterConfig {
+ private final Map<String, String> initParams;
+
+ private TestFilterConfig(Map<String, String> initParams) {
+ this.initParams = initParams;
+ }
+
+ @Override
+ public String getFilterName() {
+ return "testFilter";
+ }
+
+ @Override
+ public ServletContext getServletContext() {
+ return null;
+ }
+
+ @Override
+ public String getInitParameter(String name) {
+ return initParams.get(name);
+ }
+
+ @Override
+ public Enumeration<String> getInitParameterNames() {
+ return Collections.enumeration(initParams.keySet());
+ }
+ }
+}
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilterTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilterTest.java
new file mode 100644
index 000000000..b23023ce4
--- /dev/null
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilterTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.pdp.security;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.MDC;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+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.assertTrue;
+
+public class RangerPdpRequestContextFilterTest {
+ @Test
+ public void testUsesIncomingRequestIdAndClearsMdc() throws Exception {
+ RangerPdpRequestContextFilter filter = new RangerPdpRequestContextFilter();
+ Map<String, Object> requestAttrs = new HashMap<>();
+ Map<String, String> responseHeaders = new HashMap<>();
+ final String[] requestIdSeenInChain = new String[1];
+ String requestId = "abc-123";
+
+ HttpServletRequest req = requestProxy(requestId, requestAttrs);
+ HttpServletResponse resp = responseProxy(responseHeaders);
+ FilterChain chain = chainProxy((request, response) -> requestIdSeenInChain[0] = MDC.get(RangerPdpRequestContextFilter.MDC_REQUEST_ID));
+
+ filter.doFilter(req, resp, chain);
+
+ assertEquals(requestId, requestAttrs.get(RangerPdpRequestContextFilter.MDC_REQUEST_ID));
+ assertEquals(requestId, responseHeaders.get(RangerPdpRequestContextFilter.RES_HEADER_REQUEST_ID));
+ assertEquals(requestId, requestIdSeenInChain[0]);
+ assertNull(MDC.get(RangerPdpRequestContextFilter.MDC_REQUEST_ID));
+ }
+
+ @Test
+ public void testGeneratesRequestIdWhenMissing() throws Exception {
+ RangerPdpRequestContextFilter filter = new RangerPdpRequestContextFilter();
+ Map<String, Object> requestAttrs = new HashMap<>();
+ Map<String, String> responseHeaders = new HashMap<>();
+
+ HttpServletRequest req = requestProxy(null, requestAttrs);
+ HttpServletResponse resp = responseProxy(responseHeaders);
+ FilterChain chain = chainProxy((request, response) -> {});
+
+ filter.doFilter(req, resp, chain);
+
+ Object requestId = requestAttrs.get(RangerPdpRequestContextFilter.MDC_REQUEST_ID);
+
+ assertNotNull(requestId);
+ assertTrue(requestId.toString().length() > 10);
+ assertEquals(requestId.toString(), responseHeaders.get(RangerPdpRequestContextFilter.RES_HEADER_REQUEST_ID));
+ }
+
+ private static HttpServletRequest requestProxy(String incomingRequestId, Map<String, Object> attrs) {
+ InvocationHandler handler = (proxy, method, args) -> {
+ switch (method.getName()) {
+ case "getHeader":
+ return RangerPdpRequestContextFilter.REQ_HEADER_REQUEST_ID.equals(args[0]) ? incomingRequestId : null;
+ case "setAttribute":
+ attrs.put((String) args[0], args[1]);
+ return null;
+ case "getAttribute":
+ return attrs.get((String) args[0]);
+ default:
+ return null;
+ }
+ };
+
+ return (HttpServletRequest) Proxy.newProxyInstance(
+ HttpServletRequest.class.getClassLoader(),
+ new Class<?>[] {HttpServletRequest.class},
+ handler);
+ }
+
+ private static HttpServletResponse responseProxy(Map<String, String> headers) {
+ InvocationHandler handler = (proxy, method, args) -> {
+ if ("setHeader".equals(method.getName())) {
+ headers.put((String) args[0], (String) args[1]);
+ }
+ return null;
+ };
+
+ return (HttpServletResponse) Proxy.newProxyInstance(
+ HttpServletResponse.class.getClassLoader(),
+ new Class<?>[] {HttpServletResponse.class},
+ handler);
+ }
+
+ private static FilterChain chainProxy(ChainAction action) {
+ InvocationHandler handler = (proxy, method, args) -> {
+ if ("doFilter".equals(method.getName())) {
+ action.apply(args[0], args[1]);
+ }
+ return null;
+ };
+
+ return (FilterChain) Proxy.newProxyInstance(
+ FilterChain.class.getClassLoader(),
+ new Class<?>[] {FilterChain.class},
+ handler);
+ }
+
+ @FunctionalInterface
+ private interface ChainAction {
+ void apply(Object request, Object response) throws Exception;
+ }
+}
diff --git a/pom.xml b/pom.xml
index 2c7ad5dc6..ef1bffb40 100755
--- a/pom.xml
+++ b/pom.xml
@@ -813,6 +813,7 @@
<module>jisql</module>
<module>kms</module>
<module>knox-agent</module>
+ <module>pdp</module>
<module>plugin-atlas</module>
<module>plugin-elasticsearch</module>
<module>plugin-kafka</module>
@@ -1162,6 +1163,7 @@
<module>jisql</module>
<module>kms</module>
<module>knox-agent</module>
+ <module>pdp</module>
<module>plugin-atlas</module>
<module>plugin-elasticsearch</module>
<module>plugin-kafka</module>
@@ -1250,6 +1252,7 @@
<module>jisql</module>
<module>kms</module>
<module>knox-agent</module>
+ <module>pdp</module>
<module>plugin-atlas</module>
<module>plugin-elasticsearch</module>
<module>plugin-kafka</module>
diff --git a/ranger-examples/sample-client/src/main/python/sample_kms_client.py b/ranger-examples/sample-client/src/main/python/sample_kms_client.py
new file mode 100644
index 000000000..c7abce92b
--- /dev/null
+++ b/ranger-examples/sample-client/src/main/python/sample_kms_client.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+from apache_ranger.client.ranger_kms_client import RangerKMSClient
+from apache_ranger.client.ranger_client import HadoopSimpleAuth
+from apache_ranger.model.ranger_kms import RangerKey
+import time
+
+
+##
+## Step 1: create a client to connect to Ranger KMS
+##
+kms_url = "http://localhost:9292"
+kms_auth = HadoopSimpleAuth("keyadmin")
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# kms_auth = HTTPKerberosAuth()
+#
+# For HTTP Basic authentication
+#
+# kms_auth = ("keyadmin", "rangerR0cks!")
+
+print(f"\nUsing Ranger KMS at {kms_url}")
+
+kms_client = RangerKMSClient(kms_url, kms_auth)
+
+##
+## Step 2: call KMS APIs
+##
+kms_status = kms_client.kms_status()
+print("kms_status():", kms_status)
+print()
+
+key_name = "test_" + str(int(time.time() * 1000))
+
+key = kms_client.create_key(RangerKey({"name": key_name}))
+print("create_key(" + key_name + "):", key)
+print()
+
+rollover_key = kms_client.rollover_key(key_name, key.material)
+print("rollover_key(" + key_name + "):", rollover_key)
+print()
+
+kms_client.invalidate_cache_for_key(key_name)
+print("invalidate_cache_for_key(" + key_name + ")")
+print()
+
+key_metadata = kms_client.get_key_metadata(key_name)
+print("get_key_metadata(" + key_name + "):", key_metadata)
+print()
+
+current_key = kms_client.get_current_key(key_name)
+print("get_current_key(" + key_name + "):", current_key)
+print()
+
+encrypted_keys = kms_client.generate_encrypted_key(key_name, 2)
+print("generate_encrypted_key(" + key_name + ", 2):")
+for i in range(len(encrypted_keys)):
+ encrypted_key = encrypted_keys[i]
+ decrypted_key = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
+ reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material)
+ print(" encrypted_keys[" + str(i) + "]: ", encrypted_key)
+ print(" decrypted_key[" + str(i) + "]: ", decrypted_key)
+ print(" reencrypted_key[" + str(i) + "]:", reencrypted_key)
+print()
+
+kms_client.delete_key(key_name)
+print("delete_key(" + key_name + ")")
diff --git a/ranger-examples/sample-client/src/main/python/sample_pdp_client.py b/ranger-examples/sample-client/src/main/python/sample_pdp_client.py
new file mode 100644
index 000000000..705deb9ca
--- /dev/null
+++ b/ranger-examples/sample-client/src/main/python/sample_pdp_client.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python
+
+#
+# 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.
+
+from apache_ranger.client.ranger_pdp_client import RangerPDPClient
+from apache_ranger.model.ranger_authz import RangerAccessContext, RangerAccessInfo
+from apache_ranger.model.ranger_authz import RangerAuthzRequest, RangerMultiAuthzRequest
+from apache_ranger.model.ranger_authz import RangerResourceInfo, RangerResourcePermissionsRequest, RangerUserInfo
+
+
+##
+## Step 1: create a client to connect to Ranger PDP
+##
+pdp_url = "http://localhost:6500"
+
+# For Kerberos authentication
+#
+# from requests_kerberos import HTTPKerberosAuth
+#
+# pdp = RangerPDPClient(pdp_url, HTTPKerberosAuth())
+
+# For trusted-header authN with PDP (example only):
+#
+pdp = RangerPDPClient(pdp_url, auth=None, headers={"X-Forwarded-User": "hive"})
+
+print(f"\nUsing Ranger PDP at {pdp_url}")
+
+##
+## Step 2: call PDP authorization APIs
+##
+req = RangerAuthzRequest({
+ "requestId": "req-1",
+ "user": RangerUserInfo({"name": "alice"}),
+ "access": RangerAccessInfo({"resource": RangerResourceInfo({"name": "table:default/test_tbl1"}), "permissions": ["create"]}),
+ "context": RangerAccessContext({"serviceType": "hive", "serviceName": "dev_hive"})
+})
+
+res = pdp.authorize(req)
+
+print("authorize():")
+print(f" {req}")
+print(f" {res}")
+print()
+
+req = RangerAuthzRequest({
+ "requestId": "req-2",
+ "user": RangerUserInfo({"name": "alice"}),
+ "access": RangerAccessInfo({"resource": RangerResourceInfo({"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"]}), "permissions": ["select"]}),
+ "context": RangerAccessContext({"serviceType": "hive", "serviceName": "dev_hive"})
+})
+
+res = pdp.authorize(req)
+
+print("authorize():")
+print(f" {req}")
+print(f" {res}")
+print()
+
+req = RangerMultiAuthzRequest({
+ "requestId": "req-3",
+ "user": RangerUserInfo({"name": "alice"}),
+ "accesses": [
+ RangerAccessInfo({"resource": RangerResourceInfo({"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"], "attributes": {"OWNER": "alice"}}), "permissions": ["select"]}),
+ RangerAccessInfo({"resource": RangerResourceInfo({"name": "table:default/test_vw1"}), "permissions": ["create"]})
+ ],
+ "context": RangerAccessContext({"serviceType": "hive", "serviceName": "dev_hive"})
+})
+
+res = pdp.authorize_multi(req)
+
+print("authorize_multi():")
+print(f" {req}")
+print(f" {res}")
+print()
+
+req = RangerResourcePermissionsRequest({
+ "requestId": "req-4",
+ "resource": RangerResourceInfo({"name": "table:default/test_tbl1"}),
+ "context": RangerAccessContext({"serviceType": "hive", "serviceName": "dev_hive"})
+})
+
+res = pdp.get_resource_permissions(req)
+
+print("get_resource_permissions():")
+print(f" {req}")
+print(f" {res}")
+print()