From bcd5e5e371fa28ff4ebc6ebd74b12c95246a5441 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Thu, 26 Mar 2026 13:21:57 -0700 Subject: [PATCH 01/10] RANGER-5371: pdp server implementation --- .../ranger/authz/model/RangerAuthzResult.java | 13 +- .../authz/model/RangerMultiAuthzResult.java | 13 +- .../authz/embedded/RangerAuthzPlugin.java | 8 +- .../embedded/RangerEmbeddedAuthorizer.java | 2 +- dev-support/ranger-docker/.dockerignore | 1 + dev-support/ranger-docker/.env | 4 + .../ranger-docker/Dockerfile.ranger-pdp | 41 ++ dev-support/ranger-docker/README.md | 8 +- .../docker-compose.ranger-pdp.yml | 61 +++ .../ranger-docker/scripts/kdc/entrypoint.sh | 5 +- .../ranger-docker/scripts/pdp/logback.xml | 47 ++ .../scripts/pdp/ranger-pdp-site.xml | 235 ++++++++ .../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 | 87 +++ .../python/apache_ranger/client/__init__.py | 31 ++ .../apache_ranger/client/ranger_pdp_client.py | 90 ++++ .../main/python/apache_ranger/exceptions.py | 12 +- .../python/apache_ranger/model/__init__.py | 19 + .../apache_ranger/model/ranger_authz.py | 259 +++++++++ pdp/conf.dist/README-k8s.md | 84 +++ pdp/conf.dist/logback.xml | 35 ++ pdp/conf.dist/ranger-pdp-site.xml | 299 +++++++++++ pdp/pom.xml | 200 +++++++ pdp/scripts/ranger-pdp-services.sh | 223 ++++++++ pdp/scripts/ranger-pdp.sh | 76 +++ .../apache/ranger/pdp/RangerPdpServer.java | 290 ++++++++++ .../org/apache/ranger/pdp/RangerPdpStats.java | 111 ++++ .../ranger/pdp/RangerPdpStatusServlet.java | 148 ++++++ .../ranger/pdp/config/RangerPdpConfig.java | 328 ++++++++++++ .../ranger/pdp/config/RangerPdpConstants.java | 92 ++++ .../ranger/pdp/rest/RangerPdpApplication.java | 62 +++ .../apache/ranger/pdp/rest/RangerPdpREST.java | 503 ++++++++++++++++++ .../pdp/security/HttpHeaderAuthHandler.java | 70 +++ .../ranger/pdp/security/JwtAuthHandler.java | 105 ++++ .../pdp/security/KerberosAuthHandler.java | 265 +++++++++ .../ranger/pdp/security/PdpAuthHandler.java | 111 ++++ .../pdp/security/RangerPdpAuthFilter.java | 196 +++++++ .../RangerPdpRequestContextFilter.java | 73 +++ pdp/src/main/resources/ranger-pdp-default.xml | 392 ++++++++++++++ .../apache/ranger/pdp/RangerPdpStatsTest.java | 54 ++ .../pdp/RangerPdpStatusServletTest.java | 117 ++++ .../pdp/config/RangerPdpConfigTest.java | 51 ++ .../ranger/pdp/rest/RangerPdpRESTTest.java | 354 ++++++++++++ .../security/HttpHeaderAuthHandlerTest.java | 83 +++ .../pdp/security/KerberosAuthHandlerTest.java | 69 +++ .../pdp/security/RangerPdpAuthFilterTest.java | 98 ++++ .../RangerPdpRequestContextFilterTest.java | 132 +++++ pom.xml | 3 + 50 files changed, 5799 insertions(+), 15 deletions(-) create mode 100644 dev-support/ranger-docker/Dockerfile.ranger-pdp create mode 100644 dev-support/ranger-docker/docker-compose.ranger-pdp.yml create mode 100644 dev-support/ranger-docker/scripts/pdp/logback.xml create mode 100644 dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml create mode 100644 dev-support/ranger-docker/scripts/pdp/ranger-pdp.sh create mode 100644 distro/src/main/assembly/pdp.xml create mode 100644 intg/src/main/python/apache_ranger/client/ranger_pdp_client.py create mode 100644 intg/src/main/python/apache_ranger/model/ranger_authz.py create mode 100644 pdp/conf.dist/README-k8s.md create mode 100644 pdp/conf.dist/logback.xml create mode 100644 pdp/conf.dist/ranger-pdp-site.xml create mode 100644 pdp/pom.xml create mode 100644 pdp/scripts/ranger-pdp-services.sh create mode 100644 pdp/scripts/ranger-pdp.sh create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpApplication.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandler.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthHandler.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthHandler.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java create mode 100644 pdp/src/main/resources/ranger-pdp-default.xml create mode 100644 pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java create mode 100644 pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java create mode 100644 pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java create mode 100644 pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java create mode 100644 pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandlerTest.java create mode 100644 pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java create mode 100644 pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthFilterTest.java create mode 100644 pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilterTest.java 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 4c6470660e..0dbf6b4cb1 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 permissions) { @@ -49,6 +54,12 @@ public RangerAuthzResult(String requestId, Map permiss this.permissions = permissions; } + public RangerAuthzResult(String requestId, AccessDecision decision, Map 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 5c15c2fd6f..cae5d213ef 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 accesses) { @@ -47,6 +52,12 @@ public RangerMultiAuthzResult(String requestId, List accesses this.accesses = accesses; } + public RangerMultiAuthzResult(String requestId, AccessDecision decision, List 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 3fd84a033f..4f0be2a5d6 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 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 650c161875..365255d5b4 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 @@ -218,7 +218,7 @@ private RangerAuthzPlugin getOrCreatePlugin(String serviceName, String serviceTy LOG.debug("properties for service {}: {}", serviceName, pluginProperties); - ret = new RangerAuthzPlugin(serviceType, serviceName, pluginProperties); + ret = new RangerAuthzPlugin(serviceType, serviceName, appType, pluginProperties); plugins.put(serviceName, ret); } diff --git a/dev-support/ranger-docker/.dockerignore b/dev-support/ranger-docker/.dockerignore index 22e3ff3d96..6e66a678ee 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 03a222ec7b..ddaa765883 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 0000000000..6fbbfba48b --- /dev/null +++ b/dev-support/ranger-docker/Dockerfile.ranger-pdp @@ -0,0 +1,41 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +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 /etc/ranger/cache /etc/init.d /etc/rc2.d /etc/rc3.d \ + && touch /etc/init.d/ranger-pdp \ + && ln -s /etc/init.d/ranger-pdp /etc/rc2.d/S99ranger-pdp \ + && ln -s /etc/init.d/ranger-pdp /etc/rc2.d/K00ranger-pdp \ + && ln -s /etc/init.d/ranger-pdp /etc/rc3.d/S99ranger-pdp \ + && ln -s /etc/init.d/ranger-pdp /etc/rc3.d/K00ranger-pdp \ + && 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/ /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 63f2068305..39760f583a 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 ranger-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.xml -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.xml -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 0000000000..78015f88a2 --- /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 e920f413e5..998bad4410 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 0000000000..769a3e0323 --- /dev/null +++ b/dev-support/ranger-docker/scripts/pdp/logback.xml @@ -0,0 +1,47 @@ + + + + + + + + ${pdpLogDir}/ranger-pdp-${pdpHostname}.log + true + + %d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n + + + + + System.out + + %d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n + + + + + + + + + + + + + + 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 0000000000..0edb89baf6 --- /dev/null +++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml @@ -0,0 +1,235 @@ + + + + + + + + + + ranger.pdp.port + 6500 + + + + ranger.pdp.log.dir + /var/log/ranger/pdp + + + + + ranger.pdp.ssl.enabled + false + + + + + ranger.pdp.http2.enabled + true + + + + + ranger.pdp.http.connector.maxThreads + 200 + + + + ranger.pdp.http.connector.minSpareThreads + 20 + + + + ranger.pdp.http.connector.acceptCount + 100 + + + + ranger.pdp.http.connector.maxConnections + 10000 + + + + + ranger.pdp.auth.types + kerberos + + + + + ranger.pdp.authn.header.enabled + false + + + + + ranger.pdp.jwt.provider.url + + + + + + ranger.pdp.kerberos.spnego.principal + HTTP/ranger-pdp.rangernw@EXAMPLE.COM + + + + ranger.pdp.kerberos.spnego.keytab + /etc/keytabs/HTTP.keytab + + + + hadoop.security.auth_to_local + DEFAULT + + + + + ranger.pdp.service.dev_hdfs.delegation.users + hdfs + + + + ranger.pdp.service.dev_hive.delegation.users + hive + + + + ranger.pdp.service.dev_hbase.delegation.users + hbase + + + + ranger.pdp.service.dev_kafka.delegation.users + kafka + + + + ranger.pdp.service.dev_trino.delegation.users + trino + + + + ranger.pdp.service.dev_polaris.delegation.users + polaris + + + + ranger.pdp.service.*.delegation.users + ranger + + + + + ranger.authz.default.policy.rest.url + http://ranger.rangernw:6080 + + + + ranger.authz.default.policy.rest.client.username + admin + + + + ranger.authz.default.policy.rest.client.password + rangerR0cks! + + + + ranger.authz.default.policy.cache.dir + /etc/ranger/cache + + + + + ranger.authz.audit.is.enabled + true + + + + ranger.authz.audit.log.status.log.enabled + true + + + + ranger.authz.audit.destination.solr + true + + + + ranger.authz.audit.destination.solr.urls + http://ranger-solr.rangernw:8983/solr/ranger_audits + + + + ranger.authz.audit.destination.solr.batch.filespool.dir + /var/log/ranger/pdp/audit/solr/spool + + + + ranger.authz.audit.destination.solr.force.use.inmemory.jaas.config + true + + + + ranger.authz.audit.jaas.Client.loginModuleName + com.sun.security.auth.module.Krb5LoginModule + + + + ranger.authz.audit.jaas.Client.loginModuleControlFlag + required + + + + ranger.authz.audit.jaas.Client.option.serviceName + ranger-pdp + + + + ranger.authz.audit.jaas.Client.option.useKeyTab + true + + + + ranger.authz.audit.jaas.Client.option.storeKey + false + + + + ranger.authz.audit.jaas.Client.option.useTicketCache + true + + + + ranger.authz.audit.jaas.Client.option.keyTab + /etc/keytabs/rangerpdp.keytab + + + + ranger.authz.audit.jaas.Client.option.principal + rangerpdp/ranger-pdp.rangernw@EXAMPLE.COM + + 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 0000000000..89e2f3c094 --- /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 cb3c09a8c0..be243be0cc 100644 --- a/distro/pom.xml +++ b/distro/pom.xml @@ -297,6 +297,12 @@ ${project.version} provided + + org.apache.ranger + ranger-pdp + ${project.version} + provided + org.apache.ranger ranger-plugin-classloader @@ -489,6 +495,7 @@ src/main/assembly/tagsync.xml src/main/assembly/migration-util.xml src/main/assembly/kms.xml + src/main/assembly/pdp.xml src/main/assembly/ranger-tools.xml src/main/assembly/ranger-src.xml src/main/assembly/plugin-atlas.xml @@ -1077,6 +1084,7 @@ src/main/assembly/tagsync.xml src/main/assembly/migration-util.xml src/main/assembly/kms.xml + src/main/assembly/pdp.xml src/main/assembly/ranger-tools.xml src/main/assembly/ranger-src.xml src/main/assembly/plugin-atlas.xml diff --git a/distro/src/main/assembly/pdp.xml b/distro/src/main/assembly/pdp.xml new file mode 100644 index 0000000000..f995fa37ec --- /dev/null +++ b/distro/src/main/assembly/pdp.xml @@ -0,0 +1,207 @@ + + + + pdp + + tar.gz + + ${project.parent.name}-${project.version}-pdp + true + + + + + true + + org.apache.ranger:ranger-pdp + org.apache.ranger:credentialbuilder + org.apache.ranger:authz-embedded + org.apache.ranger:ranger-audit-core + org.apache.ranger:ranger-audit-dest-hdfs + org.apache.ranger:ranger-audit-dest-solr + org.apache.ranger:ranger-authn + org.apache.ranger:ranger-authz-api + org.apache.ranger:ranger-common-utils + org.apache.ranger:ranger-plugin-classloader + org.apache.ranger:ranger-plugins-common + org.apache.ranger:ranger-plugins-cred + org.apache.ranger:ugsync-util + + + lib + true + false + 755 + 644 + + + org.apache.tomcat.embed:tomcat-embed-core:jar:${tomcat.embed.version} + org.apache.tomcat.embed:tomcat-embed-jasper:jar:${tomcat.embed.version} + org.apache.tomcat.embed:tomcat-embed-el:jar:${tomcat.embed.version} + org.apache.tomcat:tomcat-annotations-api:jar:${tomcat.embed.version} + + + org.glassfish.jersey.core:jersey-server + org.glassfish.jersey.core:jersey-common + org.glassfish.jersey.core:jersey-client + org.glassfish.jersey.containers:jersey-container-servlet-core + org.glassfish.jersey.ext:jersey-entity-filtering + org.glassfish.jersey.inject:jersey-hk2 + org.glassfish.jersey.media:jersey-media-json-jackson + jakarta.ws.rs:jakarta.ws.rs-api + javax.inject:javax.inject + javax.validation:validation-api + + + com.sun.jersey:jersey-client:jar:${jersey-bundle.version} + com.sun.jersey:jersey-core:jar:${jersey-bundle.version} + + + org.glassfish.hk2:hk2-api + org.glassfish.hk2:hk2-locator + org.glassfish.hk2:hk2-utils + org.glassfish.hk2.external:aopalliance-repackaged + org.glassfish.hk2.external:javax.inject + org.glassfish:osgi-resource-locator + + + com.fasterxml.jackson.core:jackson-databind:jar:${fasterxml.jackson.databind.version} + com.fasterxml.jackson.core:jackson-core:jar:${fasterxml.jackson.version} + com.fasterxml.jackson.core:jackson-annotations:jar:${fasterxml.jackson.version} + com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider:jar:${fasterxml.jackson.version} + com.fasterxml.jackson.jaxrs:jackson-jaxrs-base:jar:${fasterxml.jackson.version} + com.fasterxml.jackson.module:jackson-module-jaxb-annotations:jar:${fasterxml.jackson.version} + + + com.nimbusds:nimbus-jose-jwt:jar:${nimbus-jose-jwt.version} + net.minidev:json-smart + com.nimbusds:content-type + + + org.apache.commons:commons-lang3:jar:${commons.lang3.version} + commons-codec:commons-codec:jar:${commons.codec.version} + org.apache.commons:commons-collections4:jar:${commons.collections4.version} + commons-collections:commons-collections:jar:${commons.collections.version} + org.apache.commons:commons-configuration2:jar:${commons.configuration.version} + org.apache.commons:commons-compress:jar:${commons.compress.version} + commons-io:commons-io:jar:${commons.io.version} + commons-logging:commons-logging:jar:${commons.logging.version} + org.apache.commons:commons-text:jar:${commons.text.version} + org.eclipse.jetty:jetty-client:jar:${jetty-client.version} + + + org.apache.hadoop:hadoop-auth:jar:${hadoop.version} + org.apache.hadoop:hadoop-client-api:jar:${hadoop.version} + org.apache.hadoop:hadoop-client-runtime:jar:${hadoop.version} + org.apache.hadoop.thirdparty:hadoop-shaded-guava + + + org.codehaus.woodstox:stax2-api:jar:${codehaus.woodstox.stax2api.version} + com.fasterxml.woodstox:woodstox-core:jar:${fasterxml.woodstox.version} + + + org.apache.solr:solr-solrj + org.apache.httpcomponents:httpmime:jar:${httpcomponents.httpmime.version} + org.apache.httpcomponents:httpclient:jar:${httpcomponents.httpclient.version} + org.apache.httpcomponents:httpcore:jar:${httpcomponents.httpcore.version} + org.apache.zookeeper:zookeeper:jar:${zookeeper.version} + org.noggit:noggit + + + com.kstruct:gethostname4j + net.java.dev.jna:jna + net.java.dev.jna:jna-platform + + + org.slf4j:slf4j-api:jar:${slf4j.version} + ch.qos.logback:logback-classic:jar:${logback.version} + ch.qos.logback:logback-core:jar:${logback.version} + + + + com.sun.jersey:jersey-bundle + + + + + + + + + + 755 + 600 + conf.dist + ${project.parent.basedir}/pdp/conf.dist + + + + + 750 + 640 + conf + ${project.parent.basedir}/pdp/conf.dist + + **/* + + + + + + 755 + logs + ${project.parent.basedir}/pdp/conf.dist + + **/* + + + + + + 755 + + ${project.build.directory} + + version + + 444 + + + + + + + ${project.parent.basedir}/pdp/scripts/ranger-pdp-services.sh + + ranger-pdp-services.sh + 755 + + + + + ${project.parent.basedir}/pdp/scripts/ranger-pdp.sh + + ranger-pdp + 755 + + + diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md index 90e8ff6b83..14da5a0293 100644 --- a/intg/src/main/python/README.md +++ b/intg/src/main/python/README.md @@ -122,6 +122,93 @@ print(' deleted service: id=' + str(created_service.id)) ``` +```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']}), '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') +``` + ```python test_ranger_kms.py``` ```python # test_ranger_kms.py diff --git a/intg/src/main/python/apache_ranger/client/__init__.py b/intg/src/main/python/apache_ranger/client/__init__.py index ed9d0b3c4e..e4f911e7ed 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 0000000000..c028824a73 --- /dev/null +++ b/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py @@ -0,0 +1,90 @@ +#!/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, context=None): + """ + Call POST /authz/v1/permissions + :param resource: dict-like resource payload OR RangerResourcePermissionsRequest + :param context: dict-like access context payload (ignored if 'resource' is already request object) + :return: RangerResourcePermissions + """ + req = type_coerce(resource, RangerResourcePermissionsRequest) + if req is None: + req = RangerResourcePermissionsRequest({ + 'resource': resource, + 'context': context + }) + + 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 9f19a59ac8..8a6df30a7a 100644 --- a/intg/src/main/python/apache_ranger/exceptions.py +++ b/intg/src/main/python/apache_ranger/exceptions.py @@ -38,9 +38,15 @@ def __init__(self, api, 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 + 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 ] diff --git a/intg/src/main/python/apache_ranger/model/__init__.py b/intg/src/main/python/apache_ranger/model/__init__.py index ed9d0b3c4e..ca8a51a7e9 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 0000000000..186d8bae9f --- /dev/null +++ b/intg/src/main/python/apache_ranger/model/ranger_authz.py @@ -0,0 +1,259 @@ +#!/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.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 0000000000..5d636f458b --- /dev/null +++ b/pdp/conf.dist/README-k8s.md @@ -0,0 +1,84 @@ + +# 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 policy-cache age. + +## 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 + +## 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 0000000000..94ee0595c8 --- /dev/null +++ b/pdp/conf.dist/logback.xml @@ -0,0 +1,35 @@ + + + + + + System.out + + %d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n + + + + + + + + + + + + diff --git a/pdp/conf.dist/ranger-pdp-site.xml b/pdp/conf.dist/ranger-pdp-site.xml new file mode 100644 index 0000000000..c5d8469e7e --- /dev/null +++ b/pdp/conf.dist/ranger-pdp-site.xml @@ -0,0 +1,299 @@ + + + + + + + + + + + ranger.pdp.port + 6500 + + + + ranger.pdp.log.dir + /var/log/ranger/pdp + + + + + + ranger.pdp.ssl.enabled + false + + + + ranger.pdp.ssl.keystore.file + + + + + ranger.pdp.ssl.keystore.password + + + + + ranger.pdp.ssl.keystore.type + JKS + + + + ranger.pdp.ssl.truststore.enabled + false + + + + ranger.pdp.ssl.truststore.file + + + + + ranger.pdp.ssl.truststore.password + + + + + ranger.pdp.ssl.truststore.type + JKS + + + + + + ranger.pdp.http2.enabled + true + + + + + ranger.pdp.http.connector.maxThreads + 200 + + + + ranger.pdp.http.connector.minSpareThreads + 20 + + + + ranger.pdp.http.connector.acceptCount + 100 + + + + ranger.pdp.http.connector.maxConnections + 10000 + + + + + + ranger.pdp.auth.types + header,jwt,kerberos + + + + + + ranger.pdp.authn.header.enabled + false + + + + ranger.pdp.authn.header.username + X-Forwarded-User + + + + + + ranger.pdp.jwt.provider.url + + + + + ranger.pdp.jwt.public.key + + + + + ranger.pdp.jwt.cookie.name + hadoop-jwt + + + + ranger.pdp.jwt.audiences + + + + + + + ranger.pdp.kerberos.spnego.principal + + + + + ranger.pdp.kerberos.spnego.keytab + + + + + hadoop.security.auth_to_local + DEFAULT + + + + ranger.pdp.kerberos.token.valid.seconds + 30 + + + + ranger.pdp.kerberos.cookie.domain + + + + + ranger.pdp.kerberos.cookie.path + / + + + + + ranger.pdp.service.*.allowed.users + + + + + + + + ranger.authz.default.policy.rest.url + http://localhost:6080 + + + + + ranger.authz.default.policy.rest.client.username + admin + + + + ranger.authz.default.policy.rest.client.password + admin + + + + + ranger.authz.default.policy.rest.ssl.config.file + + + + + + ranger.authz.default.ugi.initialize + false + + + + ranger.authz.default.ugi.login.type + + + + + ranger.authz.default.ugi.keytab.principal + + + + + ranger.authz.default.ugi.keytab.file + + + + + + ranger.authz.default.policy.pollIntervalMs + 30000 + + + + + ranger.authz.default.policy.cache.dir + /var/ranger/cache/pdp + + + + + ranger.authz.init.services + + + + + + + ranger.authz.audit.is.enabled + true + + + + ranger.authz.audit.destination.solr + false + + + + ranger.authz.audit.destination.solr.urls + + + + + ranger.authz.audit.destination.hdfs + false + + + + ranger.authz.audit.destination.hdfs.dir + + + + diff --git a/pdp/pom.xml b/pdp/pom.xml new file mode 100644 index 0000000000..48da6dd21b --- /dev/null +++ b/pdp/pom.xml @@ -0,0 +1,200 @@ + + + + 4.0.0 + + + org.apache.ranger + ranger + 3.0.0-SNAPSHOT + .. + + + ranger-pdp + Ranger Policy Decision Point (PDP) Server + + + 2.35 + UTF-8 + + + + + ch.qos.logback + logback-classic + ${logback.version} + + + com.fasterxml.jackson.core + jackson-databind + ${fasterxml.jackson.databind.version} + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + ${fasterxml.jackson.version} + + + com.nimbusds + nimbus-jose-jwt + ${nimbus-jose-jwt.version} + + + + com.sun.jersey + jersey-client + ${jersey-core.version} + + + com.sun.jersey + jersey-core + ${jersey-core.version} + + + commons-codec + commons-codec + ${commons.codec.version} + + + jakarta.ws.rs + jakarta.ws.rs-api + 2.1.6 + + + javax.inject + javax.inject + ${javax-inject.version} + + + javax.validation + validation-api + ${javax.validation} + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + org.apache.ranger + authz-embedded + ${project.version} + + + org.apache.ranger + ranger-authn + ${project.version} + + + org.apache.ranger + ranger-plugin-classloader + ${project.version} + + + org.apache.ranger + ranger-plugins-common + ${project.version} + + + + com.sun.jersey + jersey-bundle + + + + + org.apache.ranger + ugsync-util + ${project.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.embed.version} + + + org.apache.tomcat.embed + tomcat-embed-jasper + ${tomcat.embed.version} + + + org.glassfish.jersey.containers + jersey-container-servlet-core + ${jersey2.version} + + + org.glassfish.jersey.core + jersey-server + ${jersey2.version} + + + org.glassfish.jersey.inject + jersey-hk2 + ${jersey2.version} + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey2.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + javax.servlet + javax.servlet-api + ${javax.servlet.version} + provided + + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.apache.ranger.pdp.RangerPdpServer + + + + + + + diff --git a/pdp/scripts/ranger-pdp-services.sh b/pdp/scripts/ranger-pdp-services.sh new file mode 100644 index 0000000000..c85c818873 --- /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 777 "$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 777 "$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 /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 0000000000..15d9ebbb0b --- /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 0000000000..cbaf07f0fe --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java @@ -0,0 +1,290 @@ +/* + * 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.RangerPdpAuthFilter; +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. + * + *

Starts an embedded Apache Tomcat instance that: + *

    + *
  • Creates and initialises a {@link RangerEmbeddedAuthorizer} singleton + *
  • Exposes the three authorizer methods as REST endpoints under {@code /authz/v1/} + *
  • Enforces authentication via {@link RangerPdpAuthFilter} (Kerberos/JWT/HTTP-Header) + *
  • Optionally enables HTTP/2 ({@code Http2Protocol} upgrade on the connector) + *
+ * + *

Startup: {@code java -jar ranger-pdp.jar} + *
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 { + authorizer = new RangerEmbeddedAuthorizer(config.getAuthzProperties()); + + authorizer.init(); + + 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. + * + *

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", "want"); + } + } + + if (config.isHttp2Enabled()) { + connector.addUpgradeProtocol(new Http2Protocol()); + + LOG.info("HTTP/2 upgrade protocol registered on connector (port={})", config.getPort()); + } + + return connector; + } + + /** + * Registers {@link RangerPdpAuthFilter} 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 RangerPdpAuthFilter()); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_AUTH_TYPES, config.getAuthTypes()); + // HTTP Header auth + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_ENABLED, String.valueOf(config.isHeaderAuthnEnabled())); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_USERNAME, config.getHeaderAuthnUsername()); + // JWT bearer token auth + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_PROVIDER_URL, config.getJwtProviderUrl()); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_PUBLIC_KEY, config.getJwtPublicKey()); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_COOKIE_NAME, config.getJwtCookieName()); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_AUDIENCES, config.getJwtAudiences()); + // Kerberos / SPNEGO + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_SPNEGO_PRINCIPAL, config.getSpnegoPrincipal()); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_SPNEGO_KEYTAB, config.getSpnegoKeytab()); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_NAME_RULES, config.getKerberosNameRules()); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_TOKEN_VALIDITY, String.valueOf(config.getKerberosTokenValiditySeconds())); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_COOKIE_DOMAIN, config.getKerberosCookieDomain()); + authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_COOKIE_PATH, config.getKerberosCookiePath()); + + 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, config, RangerPdpStatusServlet.Mode.LIVE); + HttpServlet readyServlet = new RangerPdpStatusServlet(runtimeStats, config, RangerPdpStatusServlet.Mode.READY); + HttpServlet metricsServlet = new RangerPdpStatusServlet(runtimeStats, config, 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 0000000000..41e56bec1b --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.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; + +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() { + totalAuthFailures.incrementAndGet(); + } + + 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 0000000000..6471c59eb9 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java @@ -0,0 +1,148 @@ +/* + * 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.commons.lang3.StringUtils; +import org.apache.ranger.pdp.config.RangerPdpConfig; +import org.apache.ranger.pdp.config.RangerPdpConstants; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +public class RangerPdpStatusServlet extends HttpServlet { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final RangerPdpStats runtimeState; + private final RangerPdpConfig config; + private final Mode mode; + + public enum Mode { LIVE, READY, METRICS } + + public RangerPdpStatusServlet(RangerPdpStats runtimeState, RangerPdpConfig config, Mode mode) { + this.runtimeState = runtimeState; + this.config = config; + this.mode = mode; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + switch (mode) { + case LIVE: + writeLive(resp); + break; + case READY: + writeReady(resp); + break; + case METRICS: + writeMetrics(resp); + break; + default: + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } + + private void writeLive(HttpServletResponse resp) throws IOException { + Map 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("application/json"); + + MAPPER.writeValue(resp.getOutputStream(), payload); + } + + private void writeReady(HttpServletResponse resp) throws IOException { + boolean ready = runtimeState.isServerStarted() && runtimeState.isAuthorizerInitialized() && runtimeState.isAcceptingRequests(); + + Map 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("policyCacheAgeMs", getPolicyCacheAgeMs()); + + resp.setStatus(ready ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE); + resp.setContentType("application/json"); + + MAPPER.writeValue(resp.getOutputStream(), payload); + } + + private void writeMetrics(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_policy_cache_age_ms gauge\n"); + sb.append("ranger_pdp_policy_cache_age_ms ").append(getPolicyCacheAgeMs()).append('\n'); + + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/plain; version=0.0.4"); + + resp.getWriter().write(sb.toString()); + } + + private long getPolicyCacheAgeMs() { + String cacheDir = config.get(RangerPdpConstants.PROP_AUTHZ_POLICY_CACHE_DIR, null); + + if (StringUtils.isNotBlank(cacheDir)) { + File dir = new File(cacheDir); + + if (dir.exists() && dir.isDirectory()) { + File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); + + if (files != null && files.length > 0) { + long latestMtime = 0L; + + for (File f : files) { + latestMtime = Math.max(latestMtime, f.lastModified()); + } + + if (latestMtime > 0L) { + return Math.max(0L, System.currentTimeMillis() - latestMtime); + } + } + } + } + + return -1; + } +} 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 0000000000..33913e7fa9 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java @@ -0,0 +1,328 @@ +/* + * 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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +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). + * + *

Both files use the Hadoop {@code } 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. + * + *

Authentication property names: + *

    + *
  • Kerberos/SPNEGO: {@code ranger.pdp.kerberos.spnego.*} + *
  • JWT bearer token: {@code ranger.pdp.jwt.*} + *
  • HTTP header: {@code ranger.pdp.authn.header.*} + *
+ */ +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; + + public RangerPdpConfig() { + props = new Properties(); + + 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 getAuthTypes() { + return get(RangerPdpConstants.PROP_AUTH_TYPES, "header,jwt,kerberos"); + } + + // --- HTTP Header auth --- + + public boolean isHeaderAuthnEnabled() { + return getBoolean(RangerPdpConstants.PROP_HEADER_AUTHN_ENABLED, false); + } + + public String getHeaderAuthnUsername() { + return get(RangerPdpConstants.PROP_HEADER_AUTHN_USERNAME, "X-Forwarded-User"); + } + + // --- JWT bearer token auth --- + + public String getJwtProviderUrl() { + return get(RangerPdpConstants.PROP_JWT_PROVIDER_URL, ""); + } + + public String getJwtPublicKey() { + return get(RangerPdpConstants.PROP_JWT_PUBLIC_KEY, ""); + } + + public String getJwtCookieName() { + return get(RangerPdpConstants.PROP_JWT_COOKIE_NAME, "hadoop-jwt"); + } + + public String getJwtAudiences() { + return get(RangerPdpConstants.PROP_JWT_AUDIENCES, ""); + } + + // --- Kerberos / SPNEGO --- + + public String getSpnegoPrincipal() { + return get(RangerPdpConstants.PROP_SPNEGO_PRINCIPAL, ""); + } + + public String getSpnegoKeytab() { + return get(RangerPdpConstants.PROP_SPNEGO_KEYTAB, ""); + } + + public String getKerberosNameRules() { + return get(RangerPdpConstants.PROP_KRB_NAME_RULES, "DEFAULT"); + } + + public int getKerberosTokenValiditySeconds() { + return getInt(RangerPdpConstants.PROP_KRB_TOKEN_VALIDITY, 30); + } + + public String getKerberosCookieDomain() { + return get(RangerPdpConstants.PROP_KRB_COOKIE_DOMAIN, ""); + } + + public String getKerberosCookiePath() { + return get(RangerPdpConstants.PROP_KRB_COOKIE_PATH, "/"); + } + + /** + * 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 = new FileInputStream(file)) { + parseHadoopXml(in, file.getAbsolutePath()); + } catch (IOException e) { + LOG.warn("Failed to read config file: {}", file, e); + } + } + + /** + * Parses a Hadoop-style {@code } XML document and merges all + * {@code } entries into {@link #props}. Later entries override earlier + * ones, matching Hadoop's own override semantics. + * + *
+     * {@code
+     * 
+     *   
+     *     some.key
+     *     some-value
+     *   
+     * 
+     * }
+     * 
+ */ + private void parseHadoopXml(InputStream in, String source) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(in); + NodeList entries = doc.getElementsByTagName("property"); + int loaded = 0; + + for (int i = 0; i < entries.getLength(); i++) { + Element entry = (Element) entries.item(i); + String name = childText(entry, "name"); + String value = childText(entry, "value"); + + if (StringUtils.isNotBlank(name)) { + props.setProperty(name, value != null ? value : ""); + loaded++; + } + } + + LOG.info("Loaded {} properties from {}", loaded, source); + } catch (Exception e) { + LOG.warn("Failed to parse config XML: {}", source, e); + } + } + + /** Returns the trimmed text content of the first child element with the given tag name. */ + private static String childText(Element parent, String tagName) { + NodeList nodes = parent.getElementsByTagName(tagName); + + if (nodes.getLength() > 0) { + String text = nodes.item(0).getTextContent(); + + return text != null ? text.trim() : null; + } + + return null; + } + + /** + * 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) + || key.startsWith(RangerPdpConstants.PROP_SPNEGO_PREFIX) + || key.startsWith(RangerPdpConstants.PROP_HADOOP_SECURITY_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 0000000000..0a460256a0 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java @@ -0,0 +1,92 @@ +/* + * 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_AUTH_TYPE = "ranger.pdp.auth.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_AUTH_TYPES = "ranger.pdp.auth.types"; + + // HTTP header auth + public static final String PROP_HEADER_AUTHN_ENABLED = "ranger.pdp.authn.header.enabled"; + public static final String PROP_HEADER_AUTHN_USERNAME = "ranger.pdp.authn.header.username"; + + // JWT auth + public static final String PROP_JWT_PROVIDER_URL = "ranger.pdp.jwt.provider.url"; + public static final String PROP_JWT_PUBLIC_KEY = "ranger.pdp.jwt.public.key"; + public static final String PROP_JWT_COOKIE_NAME = "ranger.pdp.jwt.cookie.name"; + public static final String PROP_JWT_AUDIENCES = "ranger.pdp.jwt.audiences"; + + // Kerberos/SPNEGO auth + public static final String PROP_SPNEGO_PRINCIPAL = "ranger.pdp.kerberos.spnego.principal"; + public static final String PROP_SPNEGO_KEYTAB = "ranger.pdp.kerberos.spnego.keytab"; + public static final String PROP_KRB_NAME_RULES = "hadoop.security.auth_to_local"; + public static final String PROP_KRB_TOKEN_VALIDITY = "ranger.pdp.kerberos.token.valid.seconds"; + public static final String PROP_KRB_COOKIE_DOMAIN = "ranger.pdp.kerberos.cookie.domain"; + public static final String PROP_KRB_COOKIE_PATH = "ranger.pdp.kerberos.cookie.path"; + + // 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."; + public static final String PROP_SPNEGO_PREFIX = "ranger.spnego."; + public static final String PROP_HADOOP_SECURITY_PREFIX = "hadoop.security."; + + // 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/rest/RangerPdpApplication.java b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpApplication.java new file mode 100644 index 0000000000..42371b452d --- /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. + * + *

Registers: + *

    + *
  • {@link JacksonFeature} – Jackson 2.x JSON provider (honours all {@code @Json*} + * annotations on the {@code authz-api} model classes) + *
  • {@link AuthorizerBinder} – HK2 binder that makes the {@link RangerAuthorizer} + * instance injectable into {@link RangerPdpREST} via {@code @Inject} + *
  • {@link RangerPdpREST} – the JAX-RS resource class + *
+ */ +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 0000000000..3267a56787 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -0,0 +1,503 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +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.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 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. + * + *

All endpoints are under {@code /authz/v1} and produce/consume {@code application/json}. + * Authentication is enforced upstream by {@link RangerPdpAuthFilter}; the authenticated + * caller's identity is read from the {@link RangerPdpConstants#ATTR_AUTHENTICATED_USER} + * request attribute. + * + * + * + * + * + * + * + * + * + * + *
MethodPathRequest bodyResponse body
POST/authz/v1/authorize{@link RangerAuthzRequest}{@link RangerAuthzResult}
POST/authz/v1/authorizeMulti{@link RangerMultiAuthzRequest}{@link RangerMultiAuthzResult}
POST/authz/v1/permissions{@link RangerResourcePermissionsRequest}{@link RangerResourcePermissions}
+ */ +@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> 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 (RESPONSE_OK.equals(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(e); + } + } + + 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 accesses = request != null ? request.getAccesses() : null; + + LOG.debug("==> authorizeMulti(requestId={}, caller={}, serviceName={})", requestId, caller, serviceName); + + ret = validateCaller(caller, user, accesses, serviceName); + + if (RESPONSE_OK.equals(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(e); + } + } + + 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 (RESPONSE_OK.equals(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(e); + } + } + + 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(Response.Status.UNAUTHORIZED) + .entity(new ErrorResponse(Response.Status.UNAUTHORIZED, "Authentication required")) + .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(Response.Status.FORBIDDEN) + .entity(new ErrorResponse(Response.Status.FORBIDDEN, caller + " is not authorized")) + .build(); + } else { + ret = RESPONSE_OK; + } + } else { + ret = RESPONSE_OK; + } + } + + return ret; + } + + private Response validateCaller(String caller, RangerUserInfo user, List accesses, String serviceName) { + final Response ret; + + if (StringUtils.isBlank(caller)) { + ret = Response.status(Response.Status.UNAUTHORIZED) + .entity(new ErrorResponse(Response.Status.UNAUTHORIZED, "Authentication required")) + .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(Response.Status.FORBIDDEN) + .entity(new ErrorResponse(Response.Status.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(Response.Status.UNAUTHORIZED) + .entity(new ErrorResponse(Response.Status.UNAUTHORIZED, "Authentication required")) + .build(); + } else if (!isDelegationUserForService(serviceName, caller)) { + LOG.info("{} is not a delegation user in service {}", caller, serviceName); + + ret = Response.status(Response.Status.FORBIDDEN) + .entity(new ErrorResponse(Response.Status.FORBIDDEN, caller + " is not authorized")) + .build(); + } else { + ret = RESPONSE_OK; + } + + return ret; + } + + private boolean isDelegationNeeded(String caller, RangerUserInfo user) { + String userName = user != null ? user.getName() : null; + 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 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); + } if (status == 401 || status == 403) { // UNAUTHORIZED or FORBIDDEN + state.recordAuthFailure(); + } else if (status == 400) { + state.recordRequestBadRequest(elapsed); + } else { + state.recordRequestError(elapsed); + } + } + + /** + * Allowed-users config format: + * ranger.pdp.service..allowed.users=user1,user2 + * ranger.pdp.service.*.allowed.users=user3,user4 + * + * If no allowed-users entries are configured, this check is disabled + * (backward-compatible behavior). + */ + private boolean isDelegationUserForService(String serviceName, String userName) { + boolean ret; + Map> delegationUsersByService = this.delegationUsersByService; + + if (delegationUsersByService.isEmpty()) { + ret = false; + } else if (StringUtils.isBlank(serviceName) || StringUtils.isBlank(userName)) { + ret = false; + } else { + Set 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 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 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(Response.Status.BAD_REQUEST) + .entity(new ErrorResponse(Response.Status.BAD_REQUEST, e.getMessage())) + .build(); + } + + private Response serverError(Exception e) { + return Response.serverError() + .entity(new ErrorResponse(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage())) + .build(); + } + + @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static 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/security/HttpHeaderAuthHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandler.java new file mode 100644 index 0000000000..4bbc4e0786 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandler.java @@ -0,0 +1,70 @@ +/* + * 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.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. + * + *

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. + * + *

Security note: 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 HttpHeaderAuthHandler implements PdpAuthHandler { + private static final Logger LOG = LoggerFactory.getLogger(HttpHeaderAuthHandler.class); + + public static final String AUTH_TYPE = "HEADER"; + + private String usernameHeader; + + @Override + public void init(Properties config) { + usernameHeader = config.getProperty(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_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/JwtAuthHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthHandler.java new file mode 100644 index 0000000000..d062dde339 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthHandler.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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Properties; + +/** + * Authenticates requests using a JWT bearer token. + * + *

Checks for the token in the {@code Authorization: Bearer } header first, + * then in the configured JWT cookie. Delegates signature verification and expiry/audience + * checks to {@link RangerDefaultJwtAuthHandler} from the {@code ranger-authn} module. + * + *

Configuration keys (all prefixed with {@code ranger.pdp.auth.jwt.}): + *

    + *
  • {@code provider.url} – JWKS endpoint URL (optional if public key is set) + *
  • {@code public.key} – PEM-encoded public key (optional if provider URL is set) + *
  • {@code cookie.name} – JWT cookie name (default: {@code hadoop-jwt}) + *
  • {@code audiences} – comma-separated list of accepted audiences (optional) + *
+ */ +public class JwtAuthHandler implements PdpAuthHandler { + private static final Logger LOG = LoggerFactory.getLogger(JwtAuthHandler.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, RangerPdpAuthFilter.PARAM_JWT_PROVIDER_URL, jwtConfig, RangerDefaultJwtAuthHandler.KEY_PROVIDER_URL); + copyIfPresent(config, RangerPdpAuthFilter.PARAM_JWT_PUBLIC_KEY, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_PUBLIC_KEY); + copyIfPresent(config, RangerPdpAuthFilter.PARAM_JWT_COOKIE_NAME, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_COOKIE_NAME); + copyIfPresent(config, RangerPdpAuthFilter.PARAM_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) throws IOException { + 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/KerberosAuthHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java new file mode 100644 index 0000000000..47ddd9a4b5 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java @@ -0,0 +1,265 @@ +/* + * 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.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.io.IOException; +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Authenticates requests using Kerberos SPNEGO (HTTP Negotiate). + * + *

Uses the JDK's built-in GSSAPI/JGSS support – no external Kerberos library is required. + * The service principal and keytab must be configured via: + *

    + *
  • {@code ranger.pdp.kerberos.spnego.principal} – e.g. {@code HTTP/host.example.com@REALM} + *
  • {@code ranger.pdp.kerberos.spnego.keytab} – absolute path to the keytab file + *
  • {@code hadoop.security.auth_to_local} – Hadoop-style name rules (default: {@code DEFAULT}) + *
+ * + *

Authentication flow: + *

    + *
  1. If no {@code Authorization: Negotiate} header is present the handler returns {@code SKIP}. + *
  2. The SPNEGO token is extracted, validated via GSSAPI, and – if a response token is + * produced (mutual authentication) – written to {@code WWW-Authenticate: Negotiate }. + *
  3. On success the short-form principal name (strip {@literal @REALM} and host components) + * is returned as the authenticated user. + *
  4. On failure a {@code 401 Negotiate} challenge is sent and {@code CHALLENGE} is returned. + *
+ */ +public class KerberosAuthHandler implements PdpAuthHandler { + private static final Logger LOG = LoggerFactory.getLogger(KerberosAuthHandler.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; + + @Override + public void init(Properties config) throws Exception { + String principal = config.getProperty(RangerPdpAuthFilter.PARAM_SPNEGO_PRINCIPAL); + String keytab = config.getProperty(RangerPdpAuthFilter.PARAM_SPNEGO_KEYTAB); + + if (StringUtils.isBlank(principal) || StringUtils.isBlank(keytab)) { + throw new IllegalArgumentException("Kerberos auth requires configurations " + RangerPdpConstants.PROP_SPNEGO_PRINCIPAL + " and " + RangerPdpConstants.PROP_SPNEGO_KEYTAB); + } + + String configuredNameRules = config.getProperty(RangerPdpAuthFilter.PARAM_KRB_NAME_RULES, "DEFAULT"); + + serviceSubject = loginWithKeytab(principal, keytab); + + initializeKerberosNameRules(configuredNameRules); + + LOG.info("KerberosAuthHandler initialized; principal={}", principal); + } + + @Override + public Result authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException { + 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) () -> 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, IOException { + GSSManager manager = GSSManager.getInstance(); + GSSCredential serverCred = manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, new Oid[] {SPNEGO_OID, KRB5_OID}, GSSCredential.ACCEPT_ONLY); + GSSContext gssCtx = manager.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 ignored) { + // best-effort cleanup + } + } + } + + private void sendUnauthorized(HttpServletResponse response, byte[] outputToken) throws IOException { + 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. + * + *

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_KRB_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 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/PdpAuthHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthHandler.java new file mode 100644 index 0000000000..0138d9f23d --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthHandler.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. + * + *

Each handler is responsible for a single credential type (Kerberos, JWT, HTTP Header). + * The {@link RangerPdpAuthFilter} tries handlers in configured order and uses the first one + * that returns {@link Result.Status#AUTHENTICATED}. + */ +public interface PdpAuthHandler { + /** + * 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. + * + *

Handlers must follow this contract: + *

    + *
  • {@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. + *
  • {@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. + *
  • {@link Result.Status#SKIP} - this handler cannot process the request (the expected + * credential type is absent). The filter should try the next handler. + *
+ * + * @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/RangerPdpAuthFilter.java b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java new file mode 100644 index 0000000000..87645cbb85 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java @@ -0,0 +1,196 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.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 java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * Servlet filter that enforces authentication for all PDP REST endpoints. + * + *

Handlers are configured via the {@code ranger.pdp.auth.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. + * + *

On success the authenticated username is stored in the request attribute + * {@link RangerPdpConstants#ATTR_AUTHENTICATED_USER} so that REST resources can read it. + * + *

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 RangerPdpAuthFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(RangerPdpAuthFilter.class); + + // Auth type list — pdp-specific + public static final String PARAM_AUTH_TYPES = RangerPdpConstants.PROP_AUTH_TYPES; + + // HTTP Header auth — consistent with ranger.admin.authn.header.* in security-admin + public static final String PARAM_HEADER_AUTHN_ENABLED = RangerPdpConstants.PROP_HEADER_AUTHN_ENABLED; + public static final String PARAM_HEADER_AUTHN_USERNAME = RangerPdpConstants.PROP_HEADER_AUTHN_USERNAME; + + // JWT bearer token auth + public static final String PARAM_JWT_PROVIDER_URL = RangerPdpConstants.PROP_JWT_PROVIDER_URL; + public static final String PARAM_JWT_PUBLIC_KEY = RangerPdpConstants.PROP_JWT_PUBLIC_KEY; + public static final String PARAM_JWT_COOKIE_NAME = RangerPdpConstants.PROP_JWT_COOKIE_NAME; + public static final String PARAM_JWT_AUDIENCES = RangerPdpConstants.PROP_JWT_AUDIENCES; + + // Kerberos / SPNEGO + public static final String PARAM_SPNEGO_PRINCIPAL = RangerPdpConstants.PROP_SPNEGO_PRINCIPAL; + public static final String PARAM_SPNEGO_KEYTAB = RangerPdpConstants.PROP_SPNEGO_KEYTAB; + public static final String PARAM_KRB_NAME_RULES = RangerPdpConstants.PROP_KRB_NAME_RULES; + public static final String PARAM_KRB_TOKEN_VALIDITY = RangerPdpConstants.PROP_KRB_TOKEN_VALIDITY; + public static final String PARAM_KRB_COOKIE_DOMAIN = RangerPdpConstants.PROP_KRB_COOKIE_DOMAIN; + public static final String PARAM_KRB_COOKIE_PATH = RangerPdpConstants.PROP_KRB_COOKIE_PATH; + + private final List handlers = new ArrayList<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + Properties config = toProperties(filterConfig); + String types = filterConfig.getInitParameter(PARAM_AUTH_TYPES); + boolean headerAuthEnabled = Boolean.parseBoolean(StringUtils.defaultIfBlank(filterConfig.getInitParameter(PARAM_HEADER_AUTHN_ENABLED), "false")); + + if (StringUtils.isBlank(types)) { + types = "header,jwt,kerberos"; + } + + for (String type : types.split(",")) { + String normalizedType = type.trim().toLowerCase(); + + if ("header".equals(normalizedType) && !headerAuthEnabled) { + LOG.info("Header auth type is configured but disabled via {}=false; skipping handler registration", PARAM_HEADER_AUTHN_ENABLED); + continue; + } + + PdpAuthHandler handler = createHandler(normalizedType); + + if (handler != null) { + try { + handler.init(config); + handlers.add(handler); + + LOG.info("Registered auth handler: {}", normalizedType); + } catch (Exception e) { + throw new ServletException("Failed to initialize auth handler: " + type, e); + } + } else { + LOG.warn("Unknown auth type ignored: {}", type.trim()); + } + } + + 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 (PdpAuthHandler handler : handlers) { + PdpAuthHandler.Result result = handler.authenticate(httpReq, httpResp); + + switch (result.getStatus()) { + case AUTHENTICATED: + httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER, result.getUserName()); + httpReq.setAttribute(RangerPdpConstants.ATTR_AUTH_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 (PdpAuthHandler handler : handlers) { + String challenge = handler.getChallengeHeader(); + + if (StringUtils.isNotBlank(challenge)) { + response.addHeader("WWW-Authenticate", challenge); + } + } + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.getWriter().write("{\"code\":\"UNAUTHENTICATED\",\"message\":\"Authentication required\"}"); + } + + private PdpAuthHandler createHandler(String type) { + switch (type) { + case "header": return new HttpHeaderAuthHandler(); + case "jwt": return new JwtAuthHandler(); + case "kerberos": return new KerberosAuthHandler(); + default: return null; + } + } + + private Properties toProperties(FilterConfig filterConfig) { + Properties props = new Properties(); + + java.util.Enumeration names = filterConfig.getInitParameterNames(); + + while (names.hasMoreElements()) { + String name = names.nextElement(); + + props.setProperty(name, filterConfig.getInitParameter(name)); + } + + return props; + } +} 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 0000000000..8d9ccc16f4 --- /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 0000000000..278981cf6e --- /dev/null +++ b/pdp/src/main/resources/ranger-pdp-default.xml @@ -0,0 +1,392 @@ + + + + + + + ranger.pdp.port + 6500 + Port the PDP server listens on. + + + + ranger.pdp.log.dir + /var/log/ranger/pdp + Directory for PDP server log files. + + + + + ranger.pdp.ssl.enabled + false + Set to true to enable HTTPS. + + + + ranger.pdp.ssl.keystore.file + + Path to the keystore file (required when SSL is enabled). + + + + ranger.pdp.ssl.keystore.password + + Keystore password. + + + + ranger.pdp.ssl.keystore.type + JKS + Keystore type (JKS, PKCS12). + + + + ranger.pdp.ssl.truststore.enabled + false + Set to true to require client certificate authentication. + + + + ranger.pdp.ssl.truststore.file + + Path to the truststore file. + + + + ranger.pdp.ssl.truststore.password + + Truststore password. + + + + ranger.pdp.ssl.truststore.type + JKS + Truststore type (JKS, PKCS12). + + + + + ranger.pdp.http2.enabled + true + + Enable HTTP/2 via upgrade on the connector. + Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside HTTP/1.1. + + + + + + ranger.pdp.http.connector.maxThreads + 200 + Maximum number of worker threads handling simultaneous requests. + + + + ranger.pdp.http.connector.minSpareThreads + 20 + Minimum number of spare worker threads kept ready. + + + + ranger.pdp.http.connector.acceptCount + 100 + Queued connection backlog when all worker threads are busy. + + + + ranger.pdp.http.connector.maxConnections + 10000 + Maximum concurrent TCP connections accepted by the connector. + + + + + ranger.pdp.auth.types + header,jwt,kerberos + + Comma-separated list of authentication methods for incoming REST requests, + tried in listed order. Supported values: header, jwt, kerberos. + + + + + + ranger.pdp.authn.header.enabled + false + + Enable trusted HTTP header authentication. Use only behind a trusted proxy. + + + + + ranger.pdp.authn.header.username + X-Forwarded-User + HTTP header name from which the authenticated username is read. + + + + + ranger.pdp.jwt.provider.url + + URL of the JWT provider (used to fetch the public key). + + + + ranger.pdp.jwt.public.key + + Base64-encoded public key for verifying JWT signatures. + + + + ranger.pdp.jwt.cookie.name + hadoop-jwt + Cookie name from which a JWT bearer token may be read. + + + + ranger.pdp.jwt.audiences + + Comma-separated list of accepted JWT audiences. Empty means any audience is accepted. + + + + + ranger.pdp.kerberos.spnego.principal + + Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM. + + + + ranger.pdp.kerberos.spnego.keytab + + Path to the keytab file for the SPNEGO service principal. + + + + hadoop.security.auth_to_local + DEFAULT + Rules for mapping Kerberos principal names to short usernames. + + + + ranger.pdp.kerberos.token.valid.seconds + 30 + Validity period (seconds) for Kerberos authentication tokens. + + + + ranger.pdp.kerberos.cookie.domain + + Cookie domain used for the Kerberos authentication cookie. + + + + ranger.pdp.kerberos.cookie.path + / + Cookie path used for the Kerberos authentication cookie. + + + + + ranger.pdp.service.*.allowed.users + + Comma-separated users allowed to invoke PDP APIs for all services. + + + + + ranger.authz.app.type + ranger-pdp + Application type reported in audit records. + + + + ranger.authz.init.services + + + 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 + + + + + ranger.authz.default.policy.source.impl + org.apache.ranger.admin.client.RangerAdminRESTClient + Policy source implementation. Use RangerAdminRESTClient to download policies live from Ranger Admin. + + + + ranger.authz.default.policy.rest.url + http://localhost:6080 + + URL of the Ranger Admin server. Comma-separated for HA. + Example: http://ranger-admin-1:6080,http://ranger-admin-2:6080 + + + + + ranger.authz.default.policy.rest.ssl.config.file + + + 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 + + + + + ranger.authz.default.policy.rest.client.connection.timeoutMs + 120000 + HTTP connection timeout (ms) for calls to Ranger Admin. + + + + ranger.authz.default.policy.rest.client.read.timeoutMs + 30000 + HTTP read timeout (ms) for calls to Ranger Admin. + + + + ranger.authz.default.policy.pollIntervalMs + 30000 + How often (ms) to poll Ranger Admin for updated policies and roles. + + + + ranger.authz.default.policy.cache.dir + /var/ranger/cache/pdp + + Directory for local JSON cache files (policies, tags, roles, userstore, GDS). + The PDP can serve authorization requests from cache while Ranger Admin is unavailable. + + + + + ranger.authz.default.policy.rest.client.username + admin + Username for connecting to Ranger Admin (basic auth). + + + + ranger.authz.default.policy.rest.client.password + admin + Password for connecting to Ranger Admin (basic auth). + + + + + + ranger.authz.default.ugi.initialize + false + Set to true to enable Kerberos login for the Ranger Admin connection. + + + + ranger.authz.default.ugi.login.type + + Kerberos login type: keytab or jaas. + + + + ranger.authz.default.ugi.keytab.principal + + Kerberos principal for keytab-based login. + + + + ranger.authz.default.ugi.keytab.file + + Path to the keytab file for keytab-based login. + + + + ranger.authz.default.ugi.jaas.appconfig + + JAAS application configuration name for jaas-based login. + + + + ranger.authz.default.use.rangerGroups + false + + When true, group membership is resolved from Ranger's internal userstore + (downloaded from Ranger Admin) rather than the local OS. + + + + + + ranger.authz.audit.is.enabled + true + Master switch for audit logging. + + + + ranger.authz.audit.destination.solr + false + Enable Solr as an audit destination. + + + + ranger.authz.audit.destination.solr.urls + + Solr URL for audit logging. + + + + ranger.authz.audit.destination.hdfs + false + Enable HDFS as an audit destination. + + + + ranger.authz.audit.destination.hdfs.dir + + HDFS directory path for audit log files. + + 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 0000000000..7300e96fae --- /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(); + + assertEquals(3L, stats.getTotalRequests()); + assertEquals(1L, stats.getTotalAuthzSuccess()); + assertEquals(1L, stats.getTotalAuthzBadRequest()); + assertEquals(1L, stats.getTotalAuthzErrors()); + assertEquals(1L, stats.getTotalAuthFailures()); + assertEquals(30_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 0000000000..74f4c95724 --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ranger.pdp; + +import org.apache.ranger.pdp.config.RangerPdpConfig; +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.File; +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.nio.file.Path; +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(); + + RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(stats, new RangerPdpConfig(), 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 2")); + assertTrue(capture.body.toString().contains("ranger_pdp_auth_failures_total 1")); + } + + @Test + public void testPolicyCacheAgeMsUsesConfiguredDirectory(@TempDir Path tempDir) throws Exception { + File cacheJson = tempDir.resolve("policy_cache.json").toFile(); + + assertTrue(cacheJson.createNewFile()); + + System.setProperty(RangerPdpConstants.PROP_AUTHZ_POLICY_CACHE_DIR, tempDir.toString()); + + RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(new RangerPdpStats(), new RangerPdpConfig(), RangerPdpStatusServlet.Mode.READY); + Method method = RangerPdpStatusServlet.class.getDeclaredMethod("getPolicyCacheAgeMs"); + + method.setAccessible(true); + + long ageMs = (Long) method.invoke(servlet); + + assertTrue(ageMs >= 0L); + } + + @SuppressWarnings("unchecked") + private static T proxy(Class 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 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 0000000000..01007fe46b --- /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_HEADER_AUTHN_USERNAME); + System.clearProperty(RangerPdpConstants.PROP_PORT); + } + + @Test + public void testHeaderUserNameCanBeOverriddenBySystemProperty() { + System.setProperty(RangerPdpConstants.PROP_HEADER_AUTHN_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 0000000000..84a004bc35 --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java @@ -0,0 +1,354 @@ +/* + * 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.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 { + @Test + public void testAuthorizeReturnsUnauthorizedWhenCallerMissing() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(); + RangerPdpREST rest = createRest(authorizer, stats); + RangerAuthzRequest request = new RangerAuthzRequest(null, null, new RangerAccessContext("hive", "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(null, null, new RangerAccessContext("hive", "svc1")); + Response response = rest.authorize(request, httpRequest("bob", 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", null, null, new RangerAccessContext("hive", "svc1")); + Response response = rest.authorize(request, httpRequest("alice", 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(null, null, new RangerAccessContext("hive", "svc1")); + Response response = rest.authorize(request, httpRequest("alice", 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(null, null, new RangerAccessContext("hive", "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(null, null, new RangerAccessContext("hive", "svc1")); + Response response = rest.authorizeMulti(request, httpRequest("bob", 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", null, null, new RangerAccessContext("hive", "svc1")); + Response response = rest.authorizeMulti(request, httpRequest("alice", 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(null, null, new RangerAccessContext("hive", "svc1")); + Response response = rest.authorizeMulti(request, httpRequest("alice", 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(null, new RangerAccessContext("hive", "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(null, new RangerAccessContext("hive", "svc1")); + Response response = rest.getResourcePermissions(request, httpRequest("bob", 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, new RangerAccessContext("hive", "svc1")); + Response response = rest.getResourcePermissions(request, httpRequest("alice", 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, new RangerAccessContext("hive", "svc1")); + Response response = rest.getResourcePermissions(request, httpRequest("alice", 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 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 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, "alice"); + + 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/HttpHeaderAuthHandlerTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandlerTest.java new file mode 100644 index 0000000000..26c16ee966 --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandlerTest.java @@ -0,0 +1,83 @@ +/* + * 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 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 HttpHeaderAuthHandlerTest { + @Test + public void testAuthenticate_usesDefaultHeaderName() { + HttpHeaderAuthHandler handler = new HttpHeaderAuthHandler(); + Properties config = new Properties(); + + handler.init(config); + + HttpServletRequest request = requestWithHeader("X-Forwarded-User", "alice"); + PdpAuthHandler.Result result = handler.authenticate(request, null); + + assertEquals(PdpAuthHandler.Result.Status.AUTHENTICATED, result.getStatus()); + assertEquals("alice", result.getUserName()); + assertEquals(HttpHeaderAuthHandler.AUTH_TYPE, result.getAuthType()); + } + + @Test + public void testAuthenticate_usesConfiguredHeaderName() { + HttpHeaderAuthHandler handler = new HttpHeaderAuthHandler(); + Properties config = new Properties(); + + config.setProperty(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_USERNAME, "X-Authenticated-User"); + + handler.init(config); + + HttpServletRequest request = requestWithHeader("X-Authenticated-User", "bob"); + PdpAuthHandler.Result result = handler.authenticate(request, null); + + assertEquals(PdpAuthHandler.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 0000000000..89d835d13a --- /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() throws Exception { + KerberosName.setRules("DEFAULT"); + } + + @Test + public void testApplyNameRules_usesConfiguredRules() throws Exception { + KerberosAuthHandler handler = new KerberosAuthHandler(); + + 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 { + KerberosAuthHandler handler = new KerberosAuthHandler(); + + 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/RangerPdpAuthFilterTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthFilterTest.java new file mode 100644 index 0000000000..41192325aa --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthFilterTest.java @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +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 RangerPdpAuthFilterTest { + @Test + public void testInit_skipsHeaderHandlerWhenDisabled() { + RangerPdpAuthFilter filter = new RangerPdpAuthFilter(); + Map params = new HashMap<>(); + + params.put(RangerPdpAuthFilter.PARAM_AUTH_TYPES, "header"); + params.put(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_ENABLED, "false"); + + assertThrows(ServletException.class, () -> filter.init(new TestFilterConfig(params))); + } + + @Test + public void testInit_registersHeaderHandlerWhenEnabled() throws Exception { + RangerPdpAuthFilter filter = new RangerPdpAuthFilter(); + Map params = new HashMap<>(); + + params.put(RangerPdpAuthFilter.PARAM_AUTH_TYPES, "header"); + params.put(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_ENABLED, "true"); + + filter.init(new TestFilterConfig(params)); + + Field handlersField = RangerPdpAuthFilter.class.getDeclaredField("handlers"); + + handlersField.setAccessible(true); + + @SuppressWarnings("unchecked") + List handlers = (List) handlersField.get(filter); + + assertEquals(1, handlers.size()); + assertEquals(HttpHeaderAuthHandler.class, handlers.get(0).getClass()); + } + + private static final class TestFilterConfig implements FilterConfig { + private final Map initParams; + + private TestFilterConfig(Map 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 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 0000000000..b23023ce49 --- /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 requestAttrs = new HashMap<>(); + Map 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 requestAttrs = new HashMap<>(); + Map 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 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 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 2c7ad5dc61..ef1bffb401 100755 --- a/pom.xml +++ b/pom.xml @@ -813,6 +813,7 @@ jisql kms knox-agent + pdp plugin-atlas plugin-elasticsearch plugin-kafka @@ -1162,6 +1163,7 @@ jisql kms knox-agent + pdp plugin-atlas plugin-elasticsearch plugin-kafka @@ -1250,6 +1252,7 @@ jisql kms knox-agent + pdp plugin-atlas plugin-elasticsearch plugin-kafka From 90176fbee85bfe1c02734f51f7d20b7ffb74d1a5 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Fri, 27 Mar 2026 19:26:22 -0700 Subject: [PATCH 02/10] RANGER-5371: fix unit test failure --- pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 3267a56787..2a1e850d37 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -392,7 +392,7 @@ private void recordRequestMetrics(Response ret, long startNanos, HttpServletRequ if (status >= 200 && status < 300) { state.recordRequestSuccess(elapsed); - } if (status == 401 || status == 403) { // UNAUTHORIZED or FORBIDDEN + } else if (status == 401 || status == 403) { // UNAUTHORIZED or FORBIDDEN state.recordAuthFailure(); } else if (status == 400) { state.recordRequestBadRequest(elapsed); From 24234ec319b7a3afba66e14b3222471e99233962 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Fri, 27 Mar 2026 19:37:38 -0700 Subject: [PATCH 03/10] RANGER-5371: fix checkstyle violation --- pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java | 1 - 1 file changed, 1 deletion(-) 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 index 2a1e850d37..b0976f9fd2 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -499,5 +499,4 @@ public String getMessage() { return message; } } - } From a37f433fd5a9ca2f245507f1084bc4c52589aff9 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sat, 28 Mar 2026 01:53:31 -0700 Subject: [PATCH 04/10] RANGER-5371: addressed review suggestions --- dev-support/ranger-docker/README.md | 4 +- .../scripts/pdp/ranger-pdp-site.xml | 93 ++++---- intg/src/main/python/README.md | 2 +- .../apache_ranger/client/ranger_pdp_client.py | 14 +- pdp/conf.dist/ranger-pdp-site.xml | 202 ++++++++++++------ .../ranger/pdp/config/RangerPdpConfig.java | 2 +- .../apache/ranger/pdp/rest/RangerPdpREST.java | 12 +- pdp/src/main/resources/ranger-pdp-default.xml | 56 ++--- 8 files changed, 202 insertions(+), 183 deletions(-) diff --git a/dev-support/ranger-docker/README.md b/dev-support/ranger-docker/README.md index 39760f583a..669749ce33 100644 --- a/dev-support/ranger-docker/README.md +++ b/dev-support/ranger-docker/README.md @@ -81,7 +81,7 @@ cd dev-support/ranger-docker # 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-pdp.xml -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-pdp.xml -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: diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml index 0edb89baf6..cf459708e9 100644 --- a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml +++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml @@ -17,10 +17,7 @@ limitations under the License. --> - - - ranger.pdp.port 6500 @@ -31,6 +28,42 @@ /var/log/ranger/pdp + + ranger.pdp.service.*.delegation.users + ranger + + + + + ranger.pdp.service.dev_hdfs.delegation.users + hdfs + + + + ranger.pdp.service.dev_hive.delegation.users + hive + + + + ranger.pdp.service.dev_hbase.delegation.users + hbase + + + + ranger.pdp.service.dev_kafka.delegation.users + kafka + + + + ranger.pdp.service.dev_trino.delegation.users + trino + + + + ranger.pdp.service.dev_polaris.delegation.users + polaris + + ranger.pdp.ssl.enabled @@ -64,10 +97,7 @@ 10000 - + ranger.pdp.auth.types kerberos @@ -79,17 +109,18 @@ false - + + ranger.pdp.authn.header.username + X-Forwarded-User + + + ranger.pdp.jwt.provider.url - + ranger.pdp.kerberos.spnego.principal HTTP/ranger-pdp.rangernw@EXAMPLE.COM @@ -105,42 +136,6 @@ DEFAULT - - - ranger.pdp.service.dev_hdfs.delegation.users - hdfs - - - - ranger.pdp.service.dev_hive.delegation.users - hive - - - - ranger.pdp.service.dev_hbase.delegation.users - hbase - - - - ranger.pdp.service.dev_kafka.delegation.users - kafka - - - - ranger.pdp.service.dev_trino.delegation.users - trino - - - - ranger.pdp.service.dev_polaris.delegation.users - polaris - - - - ranger.pdp.service.*.delegation.users - ranger - - ranger.authz.default.policy.rest.url diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md index 14da5a0293..a0b6b47a95 100644 --- a/intg/src/main/python/README.md +++ b/intg/src/main/python/README.md @@ -181,7 +181,7 @@ 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']}), 'permissions': ['select']}), + 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'}) 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 index c028824a73..9268a2f4c0 100644 --- a/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py +++ b/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py @@ -60,21 +60,15 @@ def authorize_multi(self, multi_authz_request): resp = self.client_http.call_api(RangerPDPClient.AUTHORIZE_MULTI, request_data=req) return type_coerce(resp, RangerMultiAuthzResult) - def get_resource_permissions(self, resource, context=None): + def get_resource_permissions(self, resource_perm_request): """ Call POST /authz/v1/permissions - :param resource: dict-like resource payload OR RangerResourcePermissionsRequest - :param context: dict-like access context payload (ignored if 'resource' is already request object) + :param resource_perm_request: dict-like OR RangerResourcePermissionsRequest :return: RangerResourcePermissions """ - req = type_coerce(resource, RangerResourcePermissionsRequest) - if req is None: - req = RangerResourcePermissionsRequest({ - 'resource': resource, - 'context': context - }) - + 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 diff --git a/pdp/conf.dist/ranger-pdp-site.xml b/pdp/conf.dist/ranger-pdp-site.xml index c5d8469e7e..a7b9c6d2e0 100644 --- a/pdp/conf.dist/ranger-pdp-site.xml +++ b/pdp/conf.dist/ranger-pdp-site.xml @@ -17,283 +17,349 @@ limitations under the License. --> - - - - ranger.pdp.port 6500 + Port the PDP server listens on. ranger.pdp.log.dir /var/log/ranger/pdp + Directory for PDP server log files. - + + ranger.pdp.service.*.delegation.users + + Comma-separated users, allowed to call on behalf of other users in all services + + ranger.pdp.ssl.enabled false + Set to true to enable HTTPS. ranger.pdp.ssl.keystore.file + Path to the keystore file (required when SSL is enabled). ranger.pdp.ssl.keystore.password + Keystore password. ranger.pdp.ssl.keystore.type JKS + Keystore type (JKS, PKCS12). ranger.pdp.ssl.truststore.enabled false + Set to true to require client certificate authentication. ranger.pdp.ssl.truststore.file + Path to the truststore file. ranger.pdp.ssl.truststore.password + Truststore password. ranger.pdp.ssl.truststore.type JKS + Truststore type (JKS, PKCS12). - - + ranger.pdp.http2.enabled true + + Enable HTTP/2 via upgrade on the connector. + Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside HTTP/1.1. + ranger.pdp.http.connector.maxThreads 200 + Maximum number of worker threads handling simultaneous requests. ranger.pdp.http.connector.minSpareThreads 20 + Minimum number of spare worker threads kept ready. ranger.pdp.http.connector.acceptCount 100 + Queued connection backlog when all worker threads are busy. ranger.pdp.http.connector.maxConnections 10000 + Maximum concurrent TCP connections accepted by the connector. - - + ranger.pdp.auth.types - header,jwt,kerberos + jwt,kerberos + + Comma-separated list of authentication methods for incoming REST requests, + tried in listed order. Supported values: header, jwt, kerberos. + - - + ranger.pdp.authn.header.enabled false + + Enable trusted HTTP header authentication. Use only behind a trusted proxy. + ranger.pdp.authn.header.username X-Forwarded-User + HTTP header name from which the authenticated username is read. - - + ranger.pdp.jwt.provider.url + URL of the JWT provider (used to fetch the public key). ranger.pdp.jwt.public.key + Base64-encoded public key for verifying JWT signatures. ranger.pdp.jwt.cookie.name hadoop-jwt + Cookie name from which a JWT bearer token may be read. ranger.pdp.jwt.audiences + Comma-separated list of accepted JWT audiences. Empty means any audience is accepted. - - + ranger.pdp.kerberos.spnego.principal + Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM. ranger.pdp.kerberos.spnego.keytab + Path to the keytab file for the SPNEGO service principal. hadoop.security.auth_to_local DEFAULT + Rules for mapping Kerberos principal names to short usernames. ranger.pdp.kerberos.token.valid.seconds 30 + Validity period (seconds) for Kerberos authentication tokens. ranger.pdp.kerberos.cookie.domain + Cookie domain used for the Kerberos authentication cookie. ranger.pdp.kerberos.cookie.path - / + + Cookie path used for the Kerberos authentication cookie. + + + + ranger.authz.app.type + ranger-pdp + Application type reported in audit records. - - ranger.pdp.service.*.allowed.users + ranger.authz.init.services + + 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 + - + + ranger.authz.default.policy.source.impl + org.apache.ranger.admin.client.RangerAdminRESTClient + Policy source implementation. Use RangerAdminRESTClient to download policies live from Ranger Admin. + - ranger.authz.default.policy.rest.url http://localhost:6080 + + URL of the Ranger Admin server. Comma-separated for HA. + Example: http://ranger-admin-1:6080,http://ranger-admin-2:6080 + - - ranger.authz.default.policy.rest.client.username - admin + ranger.authz.default.policy.rest.ssl.config.file + + + 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 + - ranger.authz.default.policy.rest.client.password + ranger.authz.default.policy.rest.client.connection.timeoutMs + 120000 + HTTP connection timeout (ms) for calls to Ranger Admin. + + + + ranger.authz.default.policy.rest.client.read.timeoutMs + 30000 + HTTP read timeout (ms) for calls to Ranger Admin. + + + + ranger.authz.default.policy.pollIntervalMs + 30000 + How often (ms) to poll Ranger Admin for updated policies and roles. + + + + ranger.authz.default.policy.cache.dir + /var/ranger/cache/pdp + + Directory for local JSON cache files (policies, tags, roles, userstore, GDS). + The PDP can serve authorization requests from cache while Ranger Admin is unavailable. + + + + + + ranger.authz.default.policy.rest.client.username admin + Username for connecting to Ranger Admin (basic auth). - - ranger.authz.default.policy.rest.ssl.config.file - + ranger.authz.default.policy.rest.client.password + admin + Password for connecting to Ranger Admin (basic auth). - + ranger.authz.default.ugi.initialize false + Set to true to enable Kerberos login for the Ranger Admin connection. ranger.authz.default.ugi.login.type + Kerberos login type: keytab or jaas. ranger.authz.default.ugi.keytab.principal + Kerberos principal for keytab-based login. ranger.authz.default.ugi.keytab.file + Path to the keytab file for keytab-based login. - - ranger.authz.default.policy.pollIntervalMs - 30000 - - - - - ranger.authz.default.policy.cache.dir - /var/ranger/cache/pdp + ranger.authz.default.ugi.jaas.appconfig + + JAAS application configuration name for jaas-based login. - - ranger.authz.init.services - + ranger.authz.default.use.rangerGroups + false + + When true, group membership is resolved from Ranger's internal userstore + (downloaded from Ranger Admin) rather than the local OS. + - - + ranger.authz.audit.is.enabled true + Master switch for audit logging. ranger.authz.audit.destination.solr false + Enable Solr as an audit destination. ranger.authz.audit.destination.solr.urls + Solr URL for audit logging. ranger.authz.audit.destination.hdfs false + Enable HDFS as an audit destination. ranger.authz.audit.destination.hdfs.dir + HDFS directory path for audit log files. - 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 index 33913e7fa9..a0763c917b 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java @@ -138,7 +138,7 @@ public int getHttpConnectorMaxConnections() { } public String getAuthTypes() { - return get(RangerPdpConstants.PROP_AUTH_TYPES, "header,jwt,kerberos"); + return get(RangerPdpConstants.PROP_AUTH_TYPES, "jwt,kerberos"); } // --- HTTP Header auth --- 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 index b0976f9fd2..7701e71f40 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -240,7 +240,7 @@ public Response getResourcePermissions(RangerResourcePermissionsRequest request, } } - LOG.debug("==> getResourcePermissions(caller={}, serviceName={}): ret={}", caller, serviceName, ret != null ? ret.getStatus() : null); + LOG.debug("<== getResourcePermissions(caller={}, serviceName={}): ret={}", caller, serviceName, ret != null ? ret.getStatus() : null); } finally { recordRequestMetrics(ret, startNanos, httpRequest); } @@ -401,16 +401,8 @@ private void recordRequestMetrics(Response ret, long startNanos, HttpServletRequ } } - /** - * Allowed-users config format: - * ranger.pdp.service..allowed.users=user1,user2 - * ranger.pdp.service.*.allowed.users=user3,user4 - * - * If no allowed-users entries are configured, this check is disabled - * (backward-compatible behavior). - */ private boolean isDelegationUserForService(String serviceName, String userName) { - boolean ret; + final boolean ret; Map> delegationUsersByService = this.delegationUsersByService; if (delegationUsersByService.isEmpty()) { diff --git a/pdp/src/main/resources/ranger-pdp-default.xml b/pdp/src/main/resources/ranger-pdp-default.xml index 278981cf6e..6308e8cb0d 100644 --- a/pdp/src/main/resources/ranger-pdp-default.xml +++ b/pdp/src/main/resources/ranger-pdp-default.xml @@ -16,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - ranger.pdp.port @@ -30,6 +29,12 @@ Directory for PDP server log files. + + ranger.pdp.service.*.delegation.users + + Comma-separated users, allowed to call on behalf of other users in all services + + ranger.pdp.ssl.enabled @@ -114,22 +119,17 @@ Maximum concurrent TCP connections accepted by the connector. - + ranger.pdp.auth.types - header,jwt,kerberos + jwt,kerberos Comma-separated list of authentication methods for incoming REST requests, tried in listed order. Supported values: header, jwt, kerberos. - + ranger.pdp.authn.header.enabled false @@ -144,9 +144,7 @@ HTTP header name from which the authenticated username is read. - + ranger.pdp.jwt.provider.url @@ -204,29 +202,10 @@ ranger.pdp.kerberos.cookie.path - / - Cookie path used for the Kerberos authentication cookie. - - - - - ranger.pdp.service.*.allowed.users - Comma-separated users allowed to invoke PDP APIs for all services. + Cookie path used for the Kerberos authentication cookie. - ranger.authz.app.type ranger-pdp @@ -299,6 +278,7 @@ + ranger.authz.default.policy.rest.client.username admin @@ -311,11 +291,7 @@ Password for connecting to Ranger Admin (basic auth). - - + ranger.authz.default.ugi.initialize false @@ -355,11 +331,7 @@ - + ranger.authz.audit.is.enabled true From 658e90f46cce651e8a6656ad0446218e6d44f389 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sat, 28 Mar 2026 03:10:07 -0700 Subject: [PATCH 05/10] RANGER-5371: addressed review suggestions --- .../ranger-docker/Dockerfile.ranger-pdp | 7 +- .../scripts/pdp/ranger-pdp-site.xml | 6 +- pdp/conf.dist/ranger-pdp-site.xml | 33 +++++---- pdp/scripts/ranger-pdp-services.sh | 4 +- .../ranger/pdp/config/RangerPdpConfig.java | 70 +++++-------------- .../ranger/pdp/config/RangerPdpConstants.java | 24 +++---- .../apache/ranger/pdp/rest/RangerPdpREST.java | 42 ++++++----- .../ranger/pdp/security/JwtAuthHandler.java | 2 +- .../pdp/security/KerberosAuthHandler.java | 2 +- .../pdp/security/RangerPdpAuthFilter.java | 49 +++++++------ pdp/src/main/resources/ranger-pdp-default.xml | 32 ++++----- .../pdp/config/RangerPdpConfigTest.java | 4 +- 12 files changed, 122 insertions(+), 153 deletions(-) diff --git a/dev-support/ranger-docker/Dockerfile.ranger-pdp b/dev-support/ranger-docker/Dockerfile.ranger-pdp index 6fbbfba48b..871f02b549 100644 --- a/dev-support/ranger-docker/Dockerfile.ranger-pdp +++ b/dev-support/ranger-docker/Dockerfile.ranger-pdp @@ -27,12 +27,7 @@ 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 /etc/ranger/cache /etc/init.d /etc/rc2.d /etc/rc3.d \ - && touch /etc/init.d/ranger-pdp \ - && ln -s /etc/init.d/ranger-pdp /etc/rc2.d/S99ranger-pdp \ - && ln -s /etc/init.d/ranger-pdp /etc/rc2.d/K00ranger-pdp \ - && ln -s /etc/init.d/ranger-pdp /etc/rc3.d/S99ranger-pdp \ - && ln -s /etc/init.d/ranger-pdp /etc/rc3.d/K00ranger-pdp \ + && mkdir -p /var/log/ranger/pdp /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/ /etc/ranger/ \ && chmod 744 ${RANGER_SCRIPTS}/ranger-pdp.sh diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml index cf459708e9..f7ad29f8e6 100644 --- a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml +++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml @@ -116,18 +116,18 @@ - ranger.pdp.jwt.provider.url + ranger.pdp.authn.jwt.provider.url - ranger.pdp.kerberos.spnego.principal + ranger.pdp.authn.kerberos.spnego.principal HTTP/ranger-pdp.rangernw@EXAMPLE.COM - ranger.pdp.kerberos.spnego.keytab + ranger.pdp.authn.kerberos.spnego.keytab /etc/keytabs/HTTP.keytab diff --git a/pdp/conf.dist/ranger-pdp-site.xml b/pdp/conf.dist/ranger-pdp-site.xml index a7b9c6d2e0..f0424b1a4a 100644 --- a/pdp/conf.dist/ranger-pdp-site.xml +++ b/pdp/conf.dist/ranger-pdp-site.xml @@ -16,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - ranger.pdp.port @@ -147,66 +146,66 @@ - ranger.pdp.jwt.provider.url + ranger.pdp.authn.jwt.provider.url URL of the JWT provider (used to fetch the public key). - ranger.pdp.jwt.public.key + ranger.pdp.authn.jwt.public.key - Base64-encoded public key for verifying JWT signatures. + PEM-encoded public key for verifying JWT signatures. - ranger.pdp.jwt.cookie.name + ranger.pdp.authn.jwt.cookie.name hadoop-jwt Cookie name from which a JWT bearer token may be read. - ranger.pdp.jwt.audiences + ranger.pdp.authn.jwt.audiences Comma-separated list of accepted JWT audiences. Empty means any audience is accepted. - ranger.pdp.kerberos.spnego.principal + ranger.pdp.authn.kerberos.spnego.principal Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM. - ranger.pdp.kerberos.spnego.keytab + ranger.pdp.authn.kerberos.spnego.keytab Path to the keytab file for the SPNEGO service principal. - hadoop.security.auth_to_local - DEFAULT - Rules for mapping Kerberos principal names to short usernames. - - - - ranger.pdp.kerberos.token.valid.seconds + ranger.pdp.authn.kerberos.token.valid.seconds 30 Validity period (seconds) for Kerberos authentication tokens. - ranger.pdp.kerberos.cookie.domain + ranger.pdp.authn.kerberos.cookie.domain Cookie domain used for the Kerberos authentication cookie. - ranger.pdp.kerberos.cookie.path + ranger.pdp.authn.kerberos.cookie.path Cookie path used for the Kerberos authentication cookie. + + hadoop.security.auth_to_local + DEFAULT + Rules for mapping Kerberos principal names to short usernames. + + ranger.authz.app.type ranger-pdp diff --git a/pdp/scripts/ranger-pdp-services.sh b/pdp/scripts/ranger-pdp-services.sh index c85c818873..d3fba53987 100644 --- a/pdp/scripts/ranger-pdp-services.sh +++ b/pdp/scripts/ranger-pdp-services.sh @@ -75,7 +75,7 @@ if [ "${action}" == "START" ]; then if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then mkdir -p "$RANGER_PDP_LOG_DIR" - chmod 777 "$RANGER_PDP_LOG_DIR" + chmod 755 "$RANGER_PDP_LOG_DIR" fi cp="${cdir}/conf:${cdir}/dist/*:${cdir}/lib/*" @@ -144,7 +144,7 @@ elif [ "${action}" == "RUN" ]; then if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then mkdir -p "$RANGER_PDP_LOG_DIR" - chmod 777 "$RANGER_PDP_LOG_DIR" + chmod 755 "$RANGER_PDP_LOG_DIR" fi if [ -z "${RANGER_PDP_CONF_DIR}" ]; then 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 index a0763c917b..20ad3058df 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java @@ -20,16 +20,12 @@ package org.apache.ranger.pdp.config; import org.apache.commons.lang3.StringUtils; +import org.apache.ranger.plugin.util.XMLUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -58,11 +54,9 @@ public class RangerPdpConfig { 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; + private final Properties props = new Properties(); public RangerPdpConfig() { - props = new Properties(); - loadFromClasspath(DEFAULT_CONFIG_FILE); loadFromClasspath(SITE_CONFIG_FILE); @@ -142,57 +136,54 @@ public String getAuthTypes() { } // --- HTTP Header auth --- - public boolean isHeaderAuthnEnabled() { - return getBoolean(RangerPdpConstants.PROP_HEADER_AUTHN_ENABLED, false); + return getBoolean(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, false); } public String getHeaderAuthnUsername() { - return get(RangerPdpConstants.PROP_HEADER_AUTHN_USERNAME, "X-Forwarded-User"); + return get(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Forwarded-User"); } // --- JWT bearer token auth --- - public String getJwtProviderUrl() { - return get(RangerPdpConstants.PROP_JWT_PROVIDER_URL, ""); + return get(RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, ""); } public String getJwtPublicKey() { - return get(RangerPdpConstants.PROP_JWT_PUBLIC_KEY, ""); + return get(RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, ""); } public String getJwtCookieName() { - return get(RangerPdpConstants.PROP_JWT_COOKIE_NAME, "hadoop-jwt"); + return get(RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, "hadoop-jwt"); } public String getJwtAudiences() { - return get(RangerPdpConstants.PROP_JWT_AUDIENCES, ""); + return get(RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, ""); } // --- Kerberos / SPNEGO --- - public String getSpnegoPrincipal() { - return get(RangerPdpConstants.PROP_SPNEGO_PRINCIPAL, ""); + return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL, ""); } public String getSpnegoKeytab() { - return get(RangerPdpConstants.PROP_SPNEGO_KEYTAB, ""); - } - - public String getKerberosNameRules() { - return get(RangerPdpConstants.PROP_KRB_NAME_RULES, "DEFAULT"); + return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB, ""); } public int getKerberosTokenValiditySeconds() { - return getInt(RangerPdpConstants.PROP_KRB_TOKEN_VALIDITY, 30); + return getInt(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, 30); } public String getKerberosCookieDomain() { - return get(RangerPdpConstants.PROP_KRB_COOKIE_DOMAIN, ""); + return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_DOMAIN, ""); } public String getKerberosCookiePath() { - return get(RangerPdpConstants.PROP_KRB_COOKIE_PATH, "/"); + return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_PATH, "/"); + } + + public String getKerberosNameRules() { + return get(RangerPdpConstants.PROP_KRB_NAME_RULES, "DEFAULT"); } /** @@ -270,32 +261,9 @@ private void loadFromFile(File file) { * */ private void parseHadoopXml(InputStream in, String source) { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(in); - NodeList entries = doc.getElementsByTagName("property"); - int loaded = 0; - - for (int i = 0; i < entries.getLength(); i++) { - Element entry = (Element) entries.item(i); - String name = childText(entry, "name"); - String value = childText(entry, "value"); - - if (StringUtils.isNotBlank(name)) { - props.setProperty(name, value != null ? value : ""); - loaded++; - } - } + XMLUtils.loadConfig(in, props); - LOG.info("Loaded {} properties from {}", loaded, source); - } catch (Exception e) { - LOG.warn("Failed to parse config XML: {}", source, e); - } + LOG.info("Loaded {} properties from {}", props.size(), source); } /** Returns the trimmed text content of the first child element with the given tag name. */ 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 index 0a460256a0..676952c09e 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java @@ -61,22 +61,22 @@ private RangerPdpConstants() { public static final String PROP_AUTH_TYPES = "ranger.pdp.auth.types"; // HTTP header auth - public static final String PROP_HEADER_AUTHN_ENABLED = "ranger.pdp.authn.header.enabled"; - public static final String PROP_HEADER_AUTHN_USERNAME = "ranger.pdp.authn.header.username"; + public static final String PROP_AUTHN_HEADER_ENABLED = "ranger.pdp.authn.header.enabled"; + public static final String PROP_AUTHN_HEADER_USERNAME = "ranger.pdp.authn.header.username"; // JWT auth - public static final String PROP_JWT_PROVIDER_URL = "ranger.pdp.jwt.provider.url"; - public static final String PROP_JWT_PUBLIC_KEY = "ranger.pdp.jwt.public.key"; - public static final String PROP_JWT_COOKIE_NAME = "ranger.pdp.jwt.cookie.name"; - public static final String PROP_JWT_AUDIENCES = "ranger.pdp.jwt.audiences"; + public static final String PROP_AUTHN_JWT_PROVIDER_URL = "ranger.pdp.authn.jwt.provider.url"; + public static final String PROP_AUTHN_JWT_PUBLIC_KEY = "ranger.pdp.authn.jwt.public.key"; + public static final String PROP_AUTHN_JWT_COOKIE_NAME = "ranger.pdp.authn.jwt.cookie.name"; + public static final String PROP_AUTHN_JWT_AUDIENCES = "ranger.pdp.authn.jwt.audiences"; // Kerberos/SPNEGO auth - public static final String PROP_SPNEGO_PRINCIPAL = "ranger.pdp.kerberos.spnego.principal"; - public static final String PROP_SPNEGO_KEYTAB = "ranger.pdp.kerberos.spnego.keytab"; - public static final String PROP_KRB_NAME_RULES = "hadoop.security.auth_to_local"; - public static final String PROP_KRB_TOKEN_VALIDITY = "ranger.pdp.kerberos.token.valid.seconds"; - public static final String PROP_KRB_COOKIE_DOMAIN = "ranger.pdp.kerberos.cookie.domain"; - public static final String PROP_KRB_COOKIE_PATH = "ranger.pdp.kerberos.cookie.path"; + public static final String PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL = "ranger.pdp.authn.kerberos.spnego.principal"; + public static final String PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB = "ranger.pdp.authn.kerberos.spnego.keytab"; + public static final String PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY = "ranger.pdp.authn.kerberos.token.valid.seconds"; + public static final String PROP_AUTHN_KERBEROS_KRB_COOKIE_DOMAIN = "ranger.pdp.authn.kerberos.cookie.domain"; + public static final String PROP_AUTHN_KERBEROS_KRB_COOKIE_PATH = "ranger.pdp.authn.kerberos.cookie.path"; + public static final String PROP_KRB_NAME_RULES = "hadoop.security.auth_to_local"; // Authorizer/audit properties referenced by PDP code public static final String PROP_AUTHZ_POLICY_CACHE_DIR = "ranger.authz.default.policy.cache.dir"; 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 index 7701e71f40..6727f89a74 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -62,6 +62,10 @@ 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.UNAUTHORIZED; 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; @@ -145,7 +149,7 @@ public Response authorize(RangerAuthzRequest request, @Context HttpServletReques } catch (Exception e) { LOG.error("authorize(requestId={}): internal error; caller={}", requestId, caller, e); - ret = serverError(e); + ret = serverError(); } } @@ -192,7 +196,7 @@ public Response authorizeMulti(RangerMultiAuthzRequest request, @Context HttpSer } catch (Exception e) { LOG.error("authorizeMulti(requestId={}): internal error; caller={}", requestId, caller, e); - ret = serverError(e); + ret = serverError(); } } @@ -236,7 +240,7 @@ public Response getResourcePermissions(RangerResourcePermissionsRequest request, } catch (Exception e) { LOG.error("getResourcePermissions(): unexpected error; caller={}", caller, e); - ret = serverError(e); + ret = serverError(); } } @@ -270,8 +274,8 @@ private Response validateCaller(String caller, RangerUserInfo user, RangerAccess final Response ret; if (StringUtils.isBlank(caller)) { - ret = Response.status(Response.Status.UNAUTHORIZED) - .entity(new ErrorResponse(Response.Status.UNAUTHORIZED, "Authentication required")) + ret = Response.status(UNAUTHORIZED) + .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required")) .build(); } else { boolean needsDelegation = isDelegationNeeded(caller, user) || isDelegationNeeded(access); @@ -280,8 +284,8 @@ private Response validateCaller(String caller, RangerUserInfo user, RangerAccess if (!isDelegationUserForService(serviceName, caller)) { LOG.info("{} is not a delegation user in service {}", caller, serviceName); - ret = Response.status(Response.Status.FORBIDDEN) - .entity(new ErrorResponse(Response.Status.FORBIDDEN, caller + " is not authorized")) + ret = Response.status(FORBIDDEN) + .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized")) .build(); } else { ret = RESPONSE_OK; @@ -298,8 +302,8 @@ private Response validateCaller(String caller, RangerUserInfo user, ListConfiguration keys (all prefixed with {@code ranger.pdp.auth.jwt.}): + *

Configuration keys (all prefixed with {@code ranger.pdp.authn.jwt.}): *

    *
  • {@code provider.url} – JWKS endpoint URL (optional if public key is set) *
  • {@code public.key} – PEM-encoded public key (optional if provider URL is set) diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java index 47ddd9a4b5..77a2755c97 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java @@ -97,7 +97,7 @@ public void init(Properties config) throws Exception { String keytab = config.getProperty(RangerPdpAuthFilter.PARAM_SPNEGO_KEYTAB); if (StringUtils.isBlank(principal) || StringUtils.isBlank(keytab)) { - throw new IllegalArgumentException("Kerberos auth requires configurations " + RangerPdpConstants.PROP_SPNEGO_PRINCIPAL + " and " + RangerPdpConstants.PROP_SPNEGO_KEYTAB); + throw new IllegalArgumentException("Kerberos auth requires configurations " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL + " and " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB); } String configuredNameRules = config.getProperty(RangerPdpAuthFilter.PARAM_KRB_NAME_RULES, "DEFAULT"); diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java index 87645cbb85..1a78c40e91 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java @@ -59,22 +59,22 @@ public class RangerPdpAuthFilter implements Filter { public static final String PARAM_AUTH_TYPES = RangerPdpConstants.PROP_AUTH_TYPES; // HTTP Header auth — consistent with ranger.admin.authn.header.* in security-admin - public static final String PARAM_HEADER_AUTHN_ENABLED = RangerPdpConstants.PROP_HEADER_AUTHN_ENABLED; - public static final String PARAM_HEADER_AUTHN_USERNAME = RangerPdpConstants.PROP_HEADER_AUTHN_USERNAME; + public static final String PARAM_HEADER_AUTHN_ENABLED = RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED; + public static final String PARAM_HEADER_AUTHN_USERNAME = RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME; // JWT bearer token auth - public static final String PARAM_JWT_PROVIDER_URL = RangerPdpConstants.PROP_JWT_PROVIDER_URL; - public static final String PARAM_JWT_PUBLIC_KEY = RangerPdpConstants.PROP_JWT_PUBLIC_KEY; - public static final String PARAM_JWT_COOKIE_NAME = RangerPdpConstants.PROP_JWT_COOKIE_NAME; - public static final String PARAM_JWT_AUDIENCES = RangerPdpConstants.PROP_JWT_AUDIENCES; + public static final String PARAM_JWT_PROVIDER_URL = RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL; + public static final String PARAM_JWT_PUBLIC_KEY = RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY; + public static final String PARAM_JWT_COOKIE_NAME = RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME; + public static final String PARAM_JWT_AUDIENCES = RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES; // Kerberos / SPNEGO - public static final String PARAM_SPNEGO_PRINCIPAL = RangerPdpConstants.PROP_SPNEGO_PRINCIPAL; - public static final String PARAM_SPNEGO_KEYTAB = RangerPdpConstants.PROP_SPNEGO_KEYTAB; + public static final String PARAM_SPNEGO_PRINCIPAL = RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL; + public static final String PARAM_SPNEGO_KEYTAB = RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB; public static final String PARAM_KRB_NAME_RULES = RangerPdpConstants.PROP_KRB_NAME_RULES; - public static final String PARAM_KRB_TOKEN_VALIDITY = RangerPdpConstants.PROP_KRB_TOKEN_VALIDITY; - public static final String PARAM_KRB_COOKIE_DOMAIN = RangerPdpConstants.PROP_KRB_COOKIE_DOMAIN; - public static final String PARAM_KRB_COOKIE_PATH = RangerPdpConstants.PROP_KRB_COOKIE_PATH; + public static final String PARAM_KRB_TOKEN_VALIDITY = RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY; + public static final String PARAM_KRB_COOKIE_DOMAIN = RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_DOMAIN; + public static final String PARAM_KRB_COOKIE_PATH = RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_PATH; private final List handlers = new ArrayList<>(); @@ -85,7 +85,7 @@ public void init(FilterConfig filterConfig) throws ServletException { boolean headerAuthEnabled = Boolean.parseBoolean(StringUtils.defaultIfBlank(filterConfig.getInitParameter(PARAM_HEADER_AUTHN_ENABLED), "false")); if (StringUtils.isBlank(types)) { - types = "header,jwt,kerberos"; + types = "jwt,kerberos"; } for (String type : types.split(",")) { @@ -93,22 +93,25 @@ public void init(FilterConfig filterConfig) throws ServletException { if ("header".equals(normalizedType) && !headerAuthEnabled) { LOG.info("Header auth type is configured but disabled via {}=false; skipping handler registration", PARAM_HEADER_AUTHN_ENABLED); + continue; } PdpAuthHandler handler = createHandler(normalizedType); - if (handler != null) { - try { - handler.init(config); - handlers.add(handler); - - LOG.info("Registered auth handler: {}", normalizedType); - } catch (Exception e) { - throw new ServletException("Failed to initialize auth handler: " + type, e); - } - } else { - LOG.warn("Unknown auth type ignored: {}", type.trim()); + if (handler == null) { + LOG.warn("Unknown auth type ignored: {}", type); + + continue; + } + + try { + handler.init(config); + handlers.add(handler); + + LOG.info("Registered auth handler: {}", normalizedType); + } catch (Exception e) { + LOG.error("Failed to initialize auth handler: {}", type, e); } } diff --git a/pdp/src/main/resources/ranger-pdp-default.xml b/pdp/src/main/resources/ranger-pdp-default.xml index 6308e8cb0d..f0424b1a4a 100644 --- a/pdp/src/main/resources/ranger-pdp-default.xml +++ b/pdp/src/main/resources/ranger-pdp-default.xml @@ -146,66 +146,66 @@ - ranger.pdp.jwt.provider.url + ranger.pdp.authn.jwt.provider.url URL of the JWT provider (used to fetch the public key). - ranger.pdp.jwt.public.key + ranger.pdp.authn.jwt.public.key - Base64-encoded public key for verifying JWT signatures. + PEM-encoded public key for verifying JWT signatures. - ranger.pdp.jwt.cookie.name + ranger.pdp.authn.jwt.cookie.name hadoop-jwt Cookie name from which a JWT bearer token may be read. - ranger.pdp.jwt.audiences + ranger.pdp.authn.jwt.audiences Comma-separated list of accepted JWT audiences. Empty means any audience is accepted. - ranger.pdp.kerberos.spnego.principal + ranger.pdp.authn.kerberos.spnego.principal Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM. - ranger.pdp.kerberos.spnego.keytab + ranger.pdp.authn.kerberos.spnego.keytab Path to the keytab file for the SPNEGO service principal. - hadoop.security.auth_to_local - DEFAULT - Rules for mapping Kerberos principal names to short usernames. - - - - ranger.pdp.kerberos.token.valid.seconds + ranger.pdp.authn.kerberos.token.valid.seconds 30 Validity period (seconds) for Kerberos authentication tokens. - ranger.pdp.kerberos.cookie.domain + ranger.pdp.authn.kerberos.cookie.domain Cookie domain used for the Kerberos authentication cookie. - ranger.pdp.kerberos.cookie.path + ranger.pdp.authn.kerberos.cookie.path Cookie path used for the Kerberos authentication cookie. + + hadoop.security.auth_to_local + DEFAULT + Rules for mapping Kerberos principal names to short usernames. + + ranger.authz.app.type ranger-pdp 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 index 01007fe46b..79119171bc 100644 --- a/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java +++ b/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java @@ -27,13 +27,13 @@ public class RangerPdpConfigTest { @AfterEach public void clearSystemOverrides() { - System.clearProperty(RangerPdpConstants.PROP_HEADER_AUTHN_USERNAME); + System.clearProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME); System.clearProperty(RangerPdpConstants.PROP_PORT); } @Test public void testHeaderUserNameCanBeOverriddenBySystemProperty() { - System.setProperty(RangerPdpConstants.PROP_HEADER_AUTHN_USERNAME, "X-Test-User"); + System.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Test-User"); RangerPdpConfig config = new RangerPdpConfig(); From 9146fda1546fa324968fb21eb304ace6907cbe9e Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sat, 28 Mar 2026 09:26:52 -0700 Subject: [PATCH 06/10] RANGER-5371: addressed review suggestions, fixed kerberos authentication --- .../scripts/pdp/ranger-pdp-site.xml | 24 +- intg/src/main/python/README.md | 497 ++++++++++++------ .../main/python/apache_ranger/exceptions.py | 2 - .../apache_ranger/model/ranger_authz.py | 5 +- pdp/conf.dist/README-k8s.md | 19 + pdp/conf.dist/logback.xml | 3 +- pdp/conf.dist/ranger-pdp-site.xml | 32 +- .../apache/ranger/pdp/RangerPdpServer.java | 37 +- .../ranger/pdp/config/RangerPdpConfig.java | 53 +- .../ranger/pdp/config/RangerPdpConstants.java | 32 +- .../apache/ranger/pdp/rest/RangerPdpREST.java | 11 +- ...ndler.java => HttpHeaderAuthNHandler.java} | 7 +- ...tAuthHandler.java => JwtAuthNHandler.java} | 16 +- ...Handler.java => KerberosAuthNHandler.java} | 48 +- ...pAuthHandler.java => PdpAuthNHandler.java} | 4 +- ...hFilter.java => RangerPdpAuthNFilter.java} | 98 ++-- pdp/src/main/resources/ranger-pdp-default.xml | 32 +- .../ranger/pdp/rest/RangerPdpRESTTest.java | 50 +- ...t.java => HttpHeaderAuthNHandlerTest.java} | 27 +- .../pdp/security/KerberosAuthHandlerTest.java | 6 +- ...est.java => RangerPdpAuthNFilterTest.java} | 25 +- .../src/main/python/sample_kms_client.py | 86 +++ .../src/main/python/sample_pdp_client.py | 101 ++++ 23 files changed, 800 insertions(+), 415 deletions(-) rename pdp/src/main/java/org/apache/ranger/pdp/security/{HttpHeaderAuthHandler.java => HttpHeaderAuthNHandler.java} (90%) rename pdp/src/main/java/org/apache/ranger/pdp/security/{JwtAuthHandler.java => JwtAuthNHandler.java} (83%) rename pdp/src/main/java/org/apache/ranger/pdp/security/{KerberosAuthHandler.java => KerberosAuthNHandler.java} (83%) rename pdp/src/main/java/org/apache/ranger/pdp/security/{PdpAuthHandler.java => PdpAuthNHandler.java} (96%) rename pdp/src/main/java/org/apache/ranger/pdp/security/{RangerPdpAuthFilter.java => RangerPdpAuthNFilter.java} (59%) rename pdp/src/test/java/org/apache/ranger/pdp/security/{HttpHeaderAuthHandlerTest.java => HttpHeaderAuthNHandlerTest.java} (69%) rename pdp/src/test/java/org/apache/ranger/pdp/security/{RangerPdpAuthFilterTest.java => RangerPdpAuthNFilterTest.java} (73%) create mode 100644 ranger-examples/sample-client/src/main/python/sample_kms_client.py create mode 100644 ranger-examples/sample-client/src/main/python/sample_pdp_client.py diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml index f7ad29f8e6..2e94310ad1 100644 --- a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml +++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml @@ -99,14 +99,14 @@ - ranger.pdp.auth.types - kerberos + ranger.pdp.authn.types + header,jwt,kerberos ranger.pdp.authn.header.enabled - false + true @@ -116,12 +116,22 @@ + ranger.pdp.authn.jwt.enabled + false + + + ranger.pdp.authn.jwt.provider.url + ranger.pdp.authn.kerberos.enabled + true + + + ranger.pdp.authn.kerberos.spnego.principal HTTP/ranger-pdp.rangernw@EXAMPLE.COM @@ -132,14 +142,14 @@ - hadoop.security.auth_to_local + ranger.pdp.authn.kerberos.name.rules DEFAULT ranger.authz.default.policy.rest.url - http://ranger.rangernw:6080 + http://ranger:6080 @@ -175,7 +185,7 @@ ranger.authz.audit.destination.solr.urls - http://ranger-solr.rangernw:8983/solr/ranger_audits + http://ranger-solr:8983/solr/ranger_audits @@ -200,7 +210,7 @@ ranger.authz.audit.jaas.Client.option.serviceName - ranger-pdp + HTTP diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md index a0b6b47a95..d3ea7cb04e 100644 --- a/intg/src/main/python/README.md +++ b/intg/src/main/python/README.md @@ -39,6 +39,171 @@ Package Version apache-ranger 0.0.12 ``` +## Python API clients + +### Ranger Admin (`RangerClient`) + +Base URL: `http(s)://:` + +`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)://:` + +`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)://:/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 ` in request headers + +Delegation behavior: + +- if caller differs from `request.user.name`, delegation permission is required +- delegation users are configured with `ranger.pdp.service..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"}), + "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 ", + "Content-Type": "application/json" +} +payload = { + "requestId": "req-1", + "user": {"name": "alice"}, + "access": { + "resource": {"name": "table:default/test_tbl1"}, + "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,91 +287,140 @@ print(' deleted service: id=' + str(created_service.id)) ``` -```python test_ranger_pdp.py``` +```python test_ranger_user_mgmt.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 +# test_ranger_user_mgmt.py +from apache_ranger.client.ranger_client import * +from apache_ranger.utils import * +from apache_ranger.model.ranger_user_mgmt import * +from apache_ranger.client.ranger_user_mgmt_client import * +from datetime import datetime ## -## Step 1: create a client to connect to Ranger PDP +## Step 1: create a client to connect to Apache Ranger ## -pdp_url = 'http://localhost:6500' +ranger_url = 'http://localhost:6080' +ranger_auth = ('admin', 'rangerR0cks!') # For Kerberos authentication # # from requests_kerberos import HTTPKerberosAuth # -# pdp = RangerPDPClient(pdp_url, HTTPKerberosAuth()) - -# For trusted-header authN with PDP (example only): +# ranger_auth = HTTPKerberosAuth() # -pdp = RangerPDPClient(pdp_url, auth=None, headers={ 'X-Forwarded-User': 'hive' }) +# For HTTP Basic authentication +# +# ranger_auth = ('admin', 'rangerR0cks!') + +ranger = RangerClient(ranger_url, ranger_auth) +user_mgmt = RangerUserMgmtClient(ranger) + + ## -## Step 2: call PDP authorization APIs +## Step 2: Let's call User Management 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('\nListing users') -print('authorize():') -print(f' {req}') -print(f' {res}') -print('\n') +users = user_mgmt.find_users() +print(f' {len(users.list)} users found') -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'}) -}) +for user in users.list: + print(f' id: {user.id}, name: {user.name}') -res = pdp.authorize(req) -print('authorize():') -print(f' {req}') -print(f' {res}') -print('\n') +print('\nListing groups') +groups = user_mgmt.find_groups() -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'}) -}) +print(f' {len(groups.list)} groups found') -res = pdp.authorize_multi(req) +for group in groups.list: + print(f' id: {group.id}, name: {group.name}') -print('authorize_multi():') -print(f' {req}') -print(f' {res}') -print('\n') +print('\nListing group-users') +group_users = user_mgmt.find_group_users() -req = RangerResourcePermissionsRequest({ - 'requestId': 'req-4', - 'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}), - 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) -}) +print(f' {len(group_users.list)} group-users found') -res = pdp.get_resource_permissions(req) +for group_user in group_users.list: + print(f' id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}') -print('get_resource_permissions():') -print(f' {req}') -print(f' {res}') -print('\n') + +now = datetime.now() + +name_suffix = '-' + now.strftime('%Y%m%d-%H%M%S-%f') +user_name = 'test-user' + name_suffix +group_name = 'test-group' + name_suffix + + +user = RangerUser({ 'name': user_name, 'firstName': user_name, 'lastName': 'user', 'emailAddress': user_name + '@test.org', 'password': 'Welcome1', 'userRoleList': [ 'ROLE_USER' ], 'otherAttributes': '{ "dept": "test" }' }) + +print(f'\nCreating user: name={user.name}') + +created_user = user_mgmt.create_user(user) + +print(f' created user: {created_user}') + + +group = RangerGroup({ 'name': group_name, 'otherAttributes': '{ "dept": "test" }' }) + +print(f'\nCreating group: name={group.name}') + +created_group = user_mgmt.create_group(group) + +print(f' created group: {created_group}') + + +group_user = RangerGroupUser({ 'name': created_group.name, 'parentGroupId': created_group.id, 'userId': created_user.id }) + +print(f'\nAdding user {created_user.name} to group {created_group.name}') + +created_group_user = user_mgmt.create_group_user(group_user) + +print(f' created group-user: {created_group_user}') + + +print('\nListing group-users') + +group_users = user_mgmt.find_group_users() + +print(f' {len(group_users.list)} group-users found') + +for group_user in group_users.list: + print(f' id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}') + + +print(f'\nListing users for group {group.name}') + +users = user_mgmt.get_users_in_group(group.name) + +print(f' users: {users}') + + +print(f'\nListing groups for user {user.name}') + +groups = user_mgmt.get_groups_for_user(user.name) + +print(f' groups: {groups}') + + +print(f'\nDeleting group-user {created_group_user.id}') + +user_mgmt.delete_group_user_by_id(created_group_user.id) + + +print(f'\nDeleting group {group.name}') + +user_mgmt.delete_group_by_id(created_group.id, True) + + +print(f'\nDeleting user {user.name}') + +user_mgmt.delete_user_by_id(created_user.id, True) ``` ```python test_ranger_kms.py``` @@ -316,140 +530,91 @@ kms_client.delete_key(key_name) print('delete_key(' + key_name + ')') ``` -```python test_ranger_user_mgmt.py``` +```python test_ranger_pdp.py``` ```python -# test_ranger_user_mgmt.py -from apache_ranger.client.ranger_client import * -from apache_ranger.utils import * -from apache_ranger.model.ranger_user_mgmt import * -from apache_ranger.client.ranger_user_mgmt_client import * -from datetime import datetime +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 Apache Ranger +## Step 1: create a client to connect to Ranger PDP ## -ranger_url = 'http://localhost:6080' -ranger_auth = ('admin', 'rangerR0cks!') +pdp_url = 'http://localhost:6500' # For Kerberos authentication # # from requests_kerberos import HTTPKerberosAuth # -# ranger_auth = HTTPKerberosAuth() -# -# For HTTP Basic authentication -# -# ranger_auth = ('admin', 'rangerR0cks!') - -ranger = RangerClient(ranger_url, ranger_auth) -user_mgmt = RangerUserMgmtClient(ranger) - +# 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: Let's call User Management APIs +## 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'}) +}) -print('\nListing users') - -users = user_mgmt.find_users() - -print(f' {len(users.list)} users found') - -for user in users.list: - print(f' id: {user.id}, name: {user.name}') - - -print('\nListing groups') - -groups = user_mgmt.find_groups() - -print(f' {len(groups.list)} groups found') - -for group in groups.list: - print(f' id: {group.id}, name: {group.name}') - -print('\nListing group-users') - -group_users = user_mgmt.find_group_users() - -print(f' {len(group_users.list)} group-users found') - -for group_user in group_users.list: - print(f' id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}') - - -now = datetime.now() - -name_suffix = '-' + now.strftime('%Y%m%d-%H%M%S-%f') -user_name = 'test-user' + name_suffix -group_name = 'test-group' + name_suffix - - -user = RangerUser({ 'name': user_name, 'firstName': user_name, 'lastName': 'user', 'emailAddress': user_name + '@test.org', 'password': 'Welcome1', 'userRoleList': [ 'ROLE_USER' ], 'otherAttributes': '{ "dept": "test" }' }) - -print(f'\nCreating user: name={user.name}') - -created_user = user_mgmt.create_user(user) - -print(f' created user: {created_user}') - - -group = RangerGroup({ 'name': group_name, 'otherAttributes': '{ "dept": "test" }' }) - -print(f'\nCreating group: name={group.name}') - -created_group = user_mgmt.create_group(group) - -print(f' created group: {created_group}') - - -group_user = RangerGroupUser({ 'name': created_group.name, 'parentGroupId': created_group.id, 'userId': created_user.id }) - -print(f'\nAdding user {created_user.name} to group {created_group.name}') - -created_group_user = user_mgmt.create_group_user(group_user) - -print(f' created group-user: {created_group_user}') - - -print('\nListing group-users') - -group_users = user_mgmt.find_group_users() - -print(f' {len(group_users.list)} group-users found') - -for group_user in group_users.list: - print(f' id: {group_user.id}, groupId: {group_user.parentGroupId}, userId: {group_user.userId}') - - -print(f'\nListing users for group {group.name}') - -users = user_mgmt.get_users_in_group(group.name) - -print(f' users: {users}') +res = pdp.authorize(req) +print('authorize():') +print(f' {req}') +print(f' {res}') +print('\n') -print(f'\nListing groups for user {user.name}') -groups = user_mgmt.get_groups_for_user(user.name) +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'}) +}) -print(f' groups: {groups}') +res = pdp.authorize(req) +print('authorize():') +print(f' {req}') +print(f' {res}') +print('\n') -print(f'\nDeleting group-user {created_group_user.id}') -user_mgmt.delete_group_user_by_id(created_group_user.id) +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(f'\nDeleting group {group.name}') +print('authorize_multi():') +print(f' {req}') +print(f' {res}') +print('\n') -user_mgmt.delete_group_by_id(created_group.id, True) +req = RangerResourcePermissionsRequest({ + 'requestId': 'req-4', + 'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}), + 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) +}) -print(f'\nDeleting user {user.name}') +res = pdp.get_resource_permissions(req) -user_mgmt.delete_user_by_id(created_user.id, True) +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. +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/exceptions.py b/intg/src/main/python/apache_ranger/exceptions.py index 8a6df30a7a..86785c35fb 100644 --- a/intg/src/main/python/apache_ranger/exceptions.py +++ b/intg/src/main/python/apache_ranger/exceptions.py @@ -33,8 +33,6 @@ 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: diff --git a/intg/src/main/python/apache_ranger/model/ranger_authz.py b/intg/src/main/python/apache_ranger/model/ranger_authz.py index 186d8bae9f..ca4b906132 100644 --- a/intg/src/main/python/apache_ranger/model/ranger_authz.py +++ b/intg/src/main/python/apache_ranger/model/ranger_authz.py @@ -222,8 +222,9 @@ class RangerResourcePermissionsRequest(RangerBase): def __init__(self, attrs=None): attrs = non_null(attrs, {}) RangerBase.__init__(self, attrs) - self.resource = attrs.get("resource") - self.context = attrs.get("context") + 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() diff --git a/pdp/conf.dist/README-k8s.md b/pdp/conf.dist/README-k8s.md index 5d636f458b..926516913a 100644 --- a/pdp/conf.dist/README-k8s.md +++ b/pdp/conf.dist/README-k8s.md @@ -74,6 +74,25 @@ 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: diff --git a/pdp/conf.dist/logback.xml b/pdp/conf.dist/logback.xml index 94ee0595c8..90b82451a9 100644 --- a/pdp/conf.dist/logback.xml +++ b/pdp/conf.dist/logback.xml @@ -23,12 +23,13 @@ %d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n + - + diff --git a/pdp/conf.dist/ranger-pdp-site.xml b/pdp/conf.dist/ranger-pdp-site.xml index f0424b1a4a..ddf2ff9346 100644 --- a/pdp/conf.dist/ranger-pdp-site.xml +++ b/pdp/conf.dist/ranger-pdp-site.xml @@ -121,8 +121,8 @@ - ranger.pdp.auth.types - jwt,kerberos + ranger.pdp.authn.types + header,jwt,kerberos Comma-separated list of authentication methods for incoming REST requests, tried in listed order. Supported values: header, jwt, kerberos. @@ -146,6 +146,12 @@ + ranger.pdp.authn.jwt.enabled + false + Enable JWT authentication. + + + ranger.pdp.authn.jwt.provider.url URL of the JWT provider (used to fetch the public key). @@ -171,6 +177,12 @@ + ranger.pdp.authn.kerberos.enabled + false + Enable Kerberos authentication. + + + ranger.pdp.authn.kerberos.spnego.principal Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM. @@ -184,24 +196,12 @@ ranger.pdp.authn.kerberos.token.valid.seconds - 30 + 3600 Validity period (seconds) for Kerberos authentication tokens. - ranger.pdp.authn.kerberos.cookie.domain - - Cookie domain used for the Kerberos authentication cookie. - - - - ranger.pdp.authn.kerberos.cookie.path - - Cookie path used for the Kerberos authentication cookie. - - - - hadoop.security.auth_to_local + ranger.pdp.authn.kerberos.name.rules DEFAULT Rules for mapping Kerberos principal names to short usernames. diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java index cbaf07f0fe..c946caef79 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java @@ -31,7 +31,7 @@ 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.RangerPdpAuthFilter; +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; @@ -51,7 +51,7 @@ *
      *
    • Creates and initialises a {@link RangerEmbeddedAuthorizer} singleton *
    • Exposes the three authorizer methods as REST endpoints under {@code /authz/v1/} - *
    • Enforces authentication via {@link RangerPdpAuthFilter} (Kerberos/JWT/HTTP-Header) + *
    • Enforces authentication via {@link RangerPdpAuthNFilter} (Kerberos/JWT/HTTP-Header) *
    • Optionally enables HTTP/2 ({@code Http2Protocol} upgrade on the connector) *
    * @@ -200,7 +200,7 @@ private Connector createConnector() { } /** - * Registers {@link RangerPdpAuthFilter} on all {@code /authz/*} paths. + * 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. */ @@ -217,23 +217,26 @@ private void addAuthFilter(Context ctx) { reqCtxFilterMap.addURLPattern("/*"); authFilterDef.setFilterName("rangerPdpAuthFilter"); - authFilterDef.setFilter(new RangerPdpAuthFilter()); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_AUTH_TYPES, config.getAuthTypes()); + authFilterDef.setFilter(new RangerPdpAuthNFilter()); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_TYPES, config.getAuthnTypes()); + // HTTP Header auth - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_ENABLED, String.valueOf(config.isHeaderAuthnEnabled())); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_USERNAME, config.getHeaderAuthnUsername()); + 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(RangerPdpAuthFilter.PARAM_JWT_PROVIDER_URL, config.getJwtProviderUrl()); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_PUBLIC_KEY, config.getJwtPublicKey()); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_COOKIE_NAME, config.getJwtCookieName()); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_AUDIENCES, config.getJwtAudiences()); + 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(RangerPdpAuthFilter.PARAM_SPNEGO_PRINCIPAL, config.getSpnegoPrincipal()); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_SPNEGO_KEYTAB, config.getSpnegoKeytab()); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_NAME_RULES, config.getKerberosNameRules()); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_TOKEN_VALIDITY, String.valueOf(config.getKerberosTokenValiditySeconds())); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_COOKIE_DOMAIN, config.getKerberosCookieDomain()); - authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_COOKIE_PATH, config.getKerberosCookiePath()); + 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/*"); 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 index 20ad3058df..749085555b 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java @@ -21,15 +21,14 @@ 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 org.w3c.dom.Element; -import org.w3c.dom.NodeList; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.util.Properties; /** @@ -131,8 +130,8 @@ public int getHttpConnectorMaxConnections() { return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_CONNECTIONS, 10000); } - public String getAuthTypes() { - return get(RangerPdpConstants.PROP_AUTH_TYPES, "jwt,kerberos"); + public String getAuthnTypes() { + return get(RangerPdpConstants.PROP_AUTHN_TYPES, "header,jwt,kerberos"); } // --- HTTP Header auth --- @@ -145,6 +144,10 @@ public String getHeaderAuthnUsername() { } // --- 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, ""); } @@ -162,6 +165,10 @@ public String getJwtAudiences() { } // --- Kerberos / SPNEGO --- + public boolean isKerberosAuthnEnabled() { + return getBoolean(RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED, false); + } + public String getSpnegoPrincipal() { return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL, ""); } @@ -171,19 +178,11 @@ public String getSpnegoKeytab() { } public int getKerberosTokenValiditySeconds() { - return getInt(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, 30); - } - - public String getKerberosCookieDomain() { - return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_DOMAIN, ""); - } - - public String getKerberosCookiePath() { - return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_PATH, "/"); + return getInt(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, GSSCredential.INDEFINITE_LIFETIME); } public String getKerberosNameRules() { - return get(RangerPdpConstants.PROP_KRB_NAME_RULES, "DEFAULT"); + return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, "DEFAULT"); } /** @@ -237,7 +236,7 @@ private void loadFromFile(File file) { return; } - try (InputStream in = new FileInputStream(file)) { + try (InputStream in = Files.newInputStream(file.toPath())) { parseHadoopXml(in, file.getAbsolutePath()); } catch (IOException e) { LOG.warn("Failed to read config file: {}", file, e); @@ -261,22 +260,11 @@ private void loadFromFile(File file) { * */ private void parseHadoopXml(InputStream in, String source) { - XMLUtils.loadConfig(in, props); - - LOG.info("Loaded {} properties from {}", props.size(), source); - } - - /** Returns the trimmed text content of the first child element with the given tag name. */ - private static String childText(Element parent, String tagName) { - NodeList nodes = parent.getElementsByTagName(tagName); + LOG.info("Loading from {}. Properties count {}", source, props.size()); - if (nodes.getLength() > 0) { - String text = nodes.item(0).getTextContent(); - - return text != null ? text.trim() : null; - } + XMLUtils.loadConfig(in, props); - return null; + LOG.info("Loaded from {}. Properties count {}", source, props.size()); } /** @@ -285,10 +273,7 @@ private static String childText(Element parent, String tagName) { */ private void applySystemPropertyOverrides() { for (String key : System.getProperties().stringPropertyNames()) { - if (key.startsWith(RangerPdpConstants.PROP_PDP_PREFIX) - || key.startsWith(RangerPdpConstants.PROP_AUTHZ_PREFIX) - || key.startsWith(RangerPdpConstants.PROP_SPNEGO_PREFIX) - || key.startsWith(RangerPdpConstants.PROP_HADOOP_SECURITY_PREFIX)) { + 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 index 676952c09e..aa5d4203ba 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java @@ -58,33 +58,35 @@ private RangerPdpConstants() { public static final String PROP_HTTP_CONNECTOR_MAX_CONNECTIONS = "ranger.pdp.http.connector.maxConnections"; // Authentication types - public static final String PROP_AUTH_TYPES = "ranger.pdp.auth.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_ENABLED = "ranger.pdp.authn.header.enabled"; - public static final String PROP_AUTHN_HEADER_USERNAME = "ranger.pdp.authn.header.username"; + 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_PROVIDER_URL = "ranger.pdp.authn.jwt.provider.url"; - public static final String PROP_AUTHN_JWT_PUBLIC_KEY = "ranger.pdp.authn.jwt.public.key"; - public static final String PROP_AUTHN_JWT_COOKIE_NAME = "ranger.pdp.authn.jwt.cookie.name"; - public static final String PROP_AUTHN_JWT_AUDIENCES = "ranger.pdp.authn.jwt.audiences"; + 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_SPNEGO_PRINCIPAL = "ranger.pdp.authn.kerberos.spnego.principal"; - public static final String PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB = "ranger.pdp.authn.kerberos.spnego.keytab"; - public static final String PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY = "ranger.pdp.authn.kerberos.token.valid.seconds"; - public static final String PROP_AUTHN_KERBEROS_KRB_COOKIE_DOMAIN = "ranger.pdp.authn.kerberos.cookie.domain"; - public static final String PROP_AUTHN_KERBEROS_KRB_COOKIE_PATH = "ranger.pdp.authn.kerberos.cookie.path"; - public static final String PROP_KRB_NAME_RULES = "hadoop.security.auth_to_local"; + 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."; - public static final String PROP_SPNEGO_PREFIX = "ranger.spnego."; - public static final String PROP_HADOOP_SECURITY_PREFIX = "hadoop.security."; // delegation users public static final String PROP_SUFFIX_DELEGATION_USERS = ".delegation.users"; 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 index 6727f89a74..a83b9cfea8 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -66,6 +66,7 @@ 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.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; @@ -277,6 +278,10 @@ private Response validateCaller(String caller, RangerUserInfo user, RangerAccess 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); @@ -305,6 +310,10 @@ private Response validateCaller(String caller, RangerUserInfo user, List{@code audiences} – comma-separated list of accepted audiences (optional) *
*/ -public class JwtAuthHandler implements PdpAuthHandler { - private static final Logger LOG = LoggerFactory.getLogger(JwtAuthHandler.class); +public class JwtAuthNHandler implements PdpAuthNHandler { + private static final Logger LOG = LoggerFactory.getLogger(JwtAuthNHandler.class); public static final String AUTH_TYPE = "JWT"; @@ -56,10 +56,10 @@ public class JwtAuthHandler implements PdpAuthHandler { public void init(Properties config) throws Exception { Properties jwtConfig = new Properties(); - copyIfPresent(config, RangerPdpAuthFilter.PARAM_JWT_PROVIDER_URL, jwtConfig, RangerDefaultJwtAuthHandler.KEY_PROVIDER_URL); - copyIfPresent(config, RangerPdpAuthFilter.PARAM_JWT_PUBLIC_KEY, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_PUBLIC_KEY); - copyIfPresent(config, RangerPdpAuthFilter.PARAM_JWT_COOKIE_NAME, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_COOKIE_NAME); - copyIfPresent(config, RangerPdpAuthFilter.PARAM_JWT_AUDIENCES, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_AUDIENCES); + 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(); @@ -69,7 +69,7 @@ public void init(Properties config) throws Exception { } @Override - public Result authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException { + public Result authenticate(HttpServletRequest request, HttpServletResponse response) { if (!RangerDefaultJwtAuthHandler.canAuthenticateRequest(request)) { return Result.skip(); } diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java similarity index 83% rename from pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java rename to pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java index 77a2755c97..76b3a67f21 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java @@ -27,6 +27,7 @@ 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; @@ -42,7 +43,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; import java.security.PrivilegedExceptionAction; import java.util.HashMap; import java.util.Map; @@ -69,8 +69,8 @@ *
  • On failure a {@code 401 Negotiate} challenge is sent and {@code CHALLENGE} is returned. * */ -public class KerberosAuthHandler implements PdpAuthHandler { - private static final Logger LOG = LoggerFactory.getLogger(KerberosAuthHandler.class); +public class KerberosAuthNHandler implements PdpAuthNHandler { + private static final Logger LOG = LoggerFactory.getLogger(KerberosAuthNHandler.class); public static final String AUTH_TYPE = "KERBEROS"; @@ -89,28 +89,40 @@ public class KerberosAuthHandler implements PdpAuthHandler { } } - private Subject serviceSubject; + private Subject serviceSubject; + private GSSManager gssManager; + private GSSCredential serverCred; @Override public void init(Properties config) throws Exception { - String principal = config.getProperty(RangerPdpAuthFilter.PARAM_SPNEGO_PRINCIPAL); - String keytab = config.getProperty(RangerPdpAuthFilter.PARAM_SPNEGO_KEYTAB); + 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); } - String configuredNameRules = config.getProperty(RangerPdpAuthFilter.PARAM_KRB_NAME_RULES, "DEFAULT"); - serviceSubject = loginWithKeytab(principal, keytab); - initializeKerberosNameRules(configuredNameRules); + 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) () -> + gssManager.createCredential(serverName, tokenLifetime, new Oid[] {SPNEGO_OID, KRB5_OID}, GSSCredential.ACCEPT_ONLY)); - LOG.info("KerberosAuthHandler initialized; principal={}", principal); + LOG.info("KerberosAuthHandler initialized; principal={} (bound acceptor credential to configured principal)", principal); } @Override - public Result authenticate(HttpServletRequest request, HttpServletResponse response) throws IOException { + public Result authenticate(HttpServletRequest request, HttpServletResponse response) { String authHeader = request.getHeader(AUTHORIZATION); if (authHeader == null || !authHeader.startsWith(NEGOTIATE_PREFIX)) { @@ -135,10 +147,8 @@ public String getChallengeHeader() { return NEGOTIATE_PREFIX.trim(); } - private Result validateSpnegoToken(byte[] inputToken, HttpServletResponse response) throws GSSException, IOException { - GSSManager manager = GSSManager.getInstance(); - GSSCredential serverCred = manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, new Oid[] {SPNEGO_OID, KRB5_OID}, GSSCredential.ACCEPT_ONLY); - GSSContext gssCtx = manager.createContext(serverCred); + private Result validateSpnegoToken(byte[] inputToken, HttpServletResponse response) throws GSSException { + GSSContext gssCtx = gssManager.createContext(serverCred); try { byte[] outputToken = gssCtx.acceptSecContext(inputToken, 0, inputToken.length); @@ -162,13 +172,13 @@ private Result validateSpnegoToken(byte[] inputToken, HttpServletResponse respon } finally { try { gssCtx.dispose(); - } catch (GSSException ignored) { - // best-effort cleanup + } catch (GSSException excp) { + LOG.debug("GSSContext.dispose() failed. Ignored", excp); } } } - private void sendUnauthorized(HttpServletResponse response, byte[] outputToken) throws IOException { + 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)); @@ -205,7 +215,7 @@ private void initializeKerberosNameRules(String configuredRules) { KerberosName.setRules(effectiveRules); - LOG.info("Initialized Kerberos name rules: {}='{}'", RangerPdpConstants.PROP_KRB_NAME_RULES, effectiveRules); + LOG.info("Initialized Kerberos name rules: {}='{}'", RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, effectiveRules); } private String defaultShortName(String principal) { diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java similarity index 96% rename from pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthHandler.java rename to pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java index 0138d9f23d..4bb11aea5b 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthHandler.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java @@ -29,10 +29,10 @@ * Contract for PDP authentication handlers. * *

    Each handler is responsible for a single credential type (Kerberos, JWT, HTTP Header). - * The {@link RangerPdpAuthFilter} tries handlers in configured order and uses the first one + * The {@link RangerPdpAuthNFilter} tries handlers in configured order and uses the first one * that returns {@link Result.Status#AUTHENTICATED}. */ -public interface PdpAuthHandler { +public interface PdpAuthNHandler { /** * Initializes the handler with filter init parameters. * diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java similarity index 59% rename from pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java rename to pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java index 1a78c40e91..98676ba3b6 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java @@ -38,6 +38,8 @@ 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. * @@ -52,66 +54,31 @@ * sends a {@code 401} response with {@code WWW-Authenticate} headers for every * configured handler that provides a challenge. */ -public class RangerPdpAuthFilter implements Filter { - private static final Logger LOG = LoggerFactory.getLogger(RangerPdpAuthFilter.class); - - // Auth type list — pdp-specific - public static final String PARAM_AUTH_TYPES = RangerPdpConstants.PROP_AUTH_TYPES; - - // HTTP Header auth — consistent with ranger.admin.authn.header.* in security-admin - public static final String PARAM_HEADER_AUTHN_ENABLED = RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED; - public static final String PARAM_HEADER_AUTHN_USERNAME = RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME; +public class RangerPdpAuthNFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(RangerPdpAuthNFilter.class); - // JWT bearer token auth - public static final String PARAM_JWT_PROVIDER_URL = RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL; - public static final String PARAM_JWT_PUBLIC_KEY = RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY; - public static final String PARAM_JWT_COOKIE_NAME = RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME; - public static final String PARAM_JWT_AUDIENCES = RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES; - - // Kerberos / SPNEGO - public static final String PARAM_SPNEGO_PRINCIPAL = RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL; - public static final String PARAM_SPNEGO_KEYTAB = RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB; - public static final String PARAM_KRB_NAME_RULES = RangerPdpConstants.PROP_KRB_NAME_RULES; - public static final String PARAM_KRB_TOKEN_VALIDITY = RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY; - public static final String PARAM_KRB_COOKIE_DOMAIN = RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_DOMAIN; - public static final String PARAM_KRB_COOKIE_PATH = RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_PATH; - - private final List handlers = new ArrayList<>(); + private final List handlers = new ArrayList<>(); @Override public void init(FilterConfig filterConfig) throws ServletException { - Properties config = toProperties(filterConfig); - String types = filterConfig.getInitParameter(PARAM_AUTH_TYPES); - boolean headerAuthEnabled = Boolean.parseBoolean(StringUtils.defaultIfBlank(filterConfig.getInitParameter(PARAM_HEADER_AUTHN_ENABLED), "false")); + Properties config = toProperties(filterConfig); + String authnTypes = filterConfig.getInitParameter(PROP_AUTHN_TYPES); - if (StringUtils.isBlank(types)) { - types = "jwt,kerberos"; - } - - for (String type : types.split(",")) { - String normalizedType = type.trim().toLowerCase(); - - if ("header".equals(normalizedType) && !headerAuthEnabled) { - LOG.info("Header auth type is configured but disabled via {}=false; skipping handler registration", PARAM_HEADER_AUTHN_ENABLED); - - continue; - } - - PdpAuthHandler handler = createHandler(normalizedType); + for (String authnType : authnTypes.split(",")) { + PdpAuthNHandler handler = createHandler(authnType.trim().toLowerCase(), filterConfig); if (handler == null) { - LOG.warn("Unknown auth type ignored: {}", type); - continue; } try { handler.init(config); + handlers.add(handler); - LOG.info("Registered auth handler: {}", normalizedType); - } catch (Exception e) { - LOG.error("Failed to initialize auth handler: {}", type, e); + LOG.info("{}: successfully registered authentication handler", authnType); + } catch (Exception excp) { + LOG.error("{}: failed to initialize authentication handler. Handler disabled", authnType, excp); } } @@ -125,8 +92,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletRequest httpReq = (HttpServletRequest) request; HttpServletResponse httpResp = (HttpServletResponse) response; - for (PdpAuthHandler handler : handlers) { - PdpAuthHandler.Result result = handler.authenticate(httpReq, httpResp); + for (PdpAuthNHandler handler : handlers) { + PdpAuthNHandler.Result result = handler.authenticate(httpReq, httpResp); switch (result.getStatus()) { case AUTHENTICATED: @@ -161,7 +128,7 @@ public void destroy() { } private void sendUnauthenticated(HttpServletResponse response) throws IOException { - for (PdpAuthHandler handler : handlers) { + for (PdpAuthNHandler handler : handlers) { String challenge = handler.getChallengeHeader(); if (StringUtils.isNotBlank(challenge)) { @@ -174,18 +141,33 @@ private void sendUnauthenticated(HttpServletResponse response) throws IOExceptio response.getWriter().write("{\"code\":\"UNAUTHENTICATED\",\"message\":\"Authentication required\"}"); } - private PdpAuthHandler createHandler(String type) { + private PdpAuthNHandler createHandler(String type, FilterConfig filterConfig) { + final PdpAuthNHandler ret; + switch (type) { - case "header": return new HttpHeaderAuthHandler(); - case "jwt": return new JwtAuthHandler(); - case "kerberos": return new KerberosAuthHandler(); - default: return null; + 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(); - + Properties props = new Properties(); java.util.Enumeration names = filterConfig.getInitParameterNames(); while (names.hasMoreElements()) { @@ -196,4 +178,8 @@ private Properties toProperties(FilterConfig filterConfig) { return props; } + + private boolean getBoolean(FilterConfig config, String name) { + return Boolean.parseBoolean(config.getInitParameter(name)); + } } diff --git a/pdp/src/main/resources/ranger-pdp-default.xml b/pdp/src/main/resources/ranger-pdp-default.xml index f0424b1a4a..ddf2ff9346 100644 --- a/pdp/src/main/resources/ranger-pdp-default.xml +++ b/pdp/src/main/resources/ranger-pdp-default.xml @@ -121,8 +121,8 @@ - ranger.pdp.auth.types - jwt,kerberos + ranger.pdp.authn.types + header,jwt,kerberos Comma-separated list of authentication methods for incoming REST requests, tried in listed order. Supported values: header, jwt, kerberos. @@ -146,6 +146,12 @@ + ranger.pdp.authn.jwt.enabled + false + Enable JWT authentication. + + + ranger.pdp.authn.jwt.provider.url URL of the JWT provider (used to fetch the public key). @@ -171,6 +177,12 @@ + ranger.pdp.authn.kerberos.enabled + false + Enable Kerberos authentication. + + + ranger.pdp.authn.kerberos.spnego.principal Kerberos service principal for SPNEGO authentication, e.g. HTTP/host@REALM. @@ -184,24 +196,12 @@ ranger.pdp.authn.kerberos.token.valid.seconds - 30 + 3600 Validity period (seconds) for Kerberos authentication tokens. - ranger.pdp.authn.kerberos.cookie.domain - - Cookie domain used for the Kerberos authentication cookie. - - - - ranger.pdp.authn.kerberos.cookie.path - - Cookie path used for the Kerberos authentication cookie. - - - - hadoop.security.auth_to_local + ranger.pdp.authn.kerberos.name.rules DEFAULT Rules for mapping Kerberos principal names to short usernames. 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 index 84a004bc35..1221af3bbd 100644 --- a/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java +++ b/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java @@ -29,6 +29,7 @@ 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; @@ -49,12 +50,17 @@ 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(null, null, new RangerAccessContext("hive", "svc1")); + 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()); @@ -66,8 +72,8 @@ public void testAuthorizeReturnsForbiddenWhenCallerNotAllowed() throws Exception RangerPdpStats stats = new RangerPdpStats(); TestAuthorizer authorizer = new TestAuthorizer(); RangerPdpREST rest = createRest(authorizer, stats); - RangerAuthzRequest request = new RangerAuthzRequest(null, null, new RangerAccessContext("hive", "svc1")); - Response response = rest.authorize(request, httpRequest("bob", 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()); @@ -78,8 +84,8 @@ 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", null, null, new RangerAccessContext("hive", "svc1")); - Response response = rest.authorize(request, httpRequest("alice", 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()); @@ -91,8 +97,8 @@ 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(null, null, new RangerAccessContext("hive", "svc1")); - Response response = rest.authorize(request, httpRequest("alice", 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()); @@ -103,7 +109,7 @@ public void testAuthorizeMultiReturnsUnauthorizedWhenCallerMissing() throws Exce RangerPdpStats stats = new RangerPdpStats(); TestAuthorizer authorizer = new TestAuthorizer(); RangerPdpREST rest = createRest(authorizer, stats); - RangerMultiAuthzRequest request = new RangerMultiAuthzRequest(null, null, new RangerAccessContext("hive", "svc1")); + 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()); @@ -115,8 +121,8 @@ public void testAuthorizeMultiReturnsForbiddenWhenCallerNotAllowed() throws Exce RangerPdpStats stats = new RangerPdpStats(); TestAuthorizer authorizer = new TestAuthorizer(); RangerPdpREST rest = createRest(authorizer, stats); - RangerMultiAuthzRequest request = new RangerMultiAuthzRequest(null, null, new RangerAccessContext("hive", "svc1")); - Response response = rest.authorizeMulti(request, httpRequest("bob", 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()); @@ -127,8 +133,8 @@ 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", null, null, new RangerAccessContext("hive", "svc1")); - Response response = rest.authorizeMulti(request, httpRequest("alice", 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()); @@ -140,8 +146,8 @@ public void testAuthorizeMultiReturnsBadRequestOnAuthzException() throws Excepti 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(null, null, new RangerAccessContext("hive", "svc1")); - Response response = rest.authorizeMulti(request, httpRequest("alice", 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()); @@ -152,7 +158,7 @@ public void testGetResourcePermissionsReturnsUnauthorizedWhenCallerMissing() thr RangerPdpStats stats = new RangerPdpStats(); TestAuthorizer authorizer = new TestAuthorizer(); RangerPdpREST rest = createRest(authorizer, stats); - RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest(null, new RangerAccessContext("hive", "svc1")); + 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()); @@ -164,8 +170,8 @@ public void testGetResourcePermissionsReturnsForbiddenWhenCallerNotAllowed() thr RangerPdpStats stats = new RangerPdpStats(); TestAuthorizer authorizer = new TestAuthorizer(); RangerPdpREST rest = createRest(authorizer, stats); - RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest(null, new RangerAccessContext("hive", "svc1")); - Response response = rest.getResourcePermissions(request, httpRequest("bob", 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()); @@ -176,8 +182,8 @@ public void testGetResourcePermissionsReturnsOkAndRecordsSuccess() throws Except RangerPdpStats stats = new RangerPdpStats(); TestAuthorizer authorizer = new TestAuthorizer(new RangerResourcePermissions()); RangerPdpREST rest = createRest(authorizer, stats); - RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, new RangerAccessContext("hive", "svc1")); - Response response = rest.getResourcePermissions(request, httpRequest("alice", 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()); @@ -189,8 +195,8 @@ public void testGetResourcePermissionsReturnsBadRequestOnAuthzException() throws 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, new RangerAccessContext("hive", "svc1")); - Response response = rest.getResourcePermissions(request, httpRequest("alice", 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()); @@ -263,7 +269,7 @@ private static void setField(Object target, String fieldName, Object value) thro private static Properties defaultConfig() { Properties ret = new Properties(); - ret.setProperty(PROP_PDP_SERVICE_PREFIX + "svc1" + PROP_SUFFIX_DELEGATION_USERS, "alice"); + ret.setProperty(PROP_PDP_SERVICE_PREFIX + "svc1" + PROP_SUFFIX_DELEGATION_USERS, CALLER_DELEGATION_ALLOWED); return ret; } diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandlerTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java similarity index 69% rename from pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandlerTest.java rename to pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java index 26c16ee966..c497be9086 100644 --- a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthHandlerTest.java +++ b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java @@ -19,6 +19,7 @@ package org.apache.ranger.pdp.security; +import org.apache.ranger.pdp.config.RangerPdpConstants; import org.junit.jupiter.api.Test; import javax.servlet.http.HttpServletRequest; @@ -29,35 +30,35 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -public class HttpHeaderAuthHandlerTest { +public class HttpHeaderAuthNHandlerTest { @Test public void testAuthenticate_usesDefaultHeaderName() { - HttpHeaderAuthHandler handler = new HttpHeaderAuthHandler(); - Properties config = new Properties(); + HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler(); + Properties config = new Properties(); handler.init(config); - HttpServletRequest request = requestWithHeader("X-Forwarded-User", "alice"); - PdpAuthHandler.Result result = handler.authenticate(request, null); + HttpServletRequest request = requestWithHeader("X-Forwarded-User", "alice"); + PdpAuthNHandler.Result result = handler.authenticate(request, null); - assertEquals(PdpAuthHandler.Result.Status.AUTHENTICATED, result.getStatus()); + assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus()); assertEquals("alice", result.getUserName()); - assertEquals(HttpHeaderAuthHandler.AUTH_TYPE, result.getAuthType()); + assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE, result.getAuthType()); } @Test public void testAuthenticate_usesConfiguredHeaderName() { - HttpHeaderAuthHandler handler = new HttpHeaderAuthHandler(); - Properties config = new Properties(); + HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler(); + Properties config = new Properties(); - config.setProperty(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_USERNAME, "X-Authenticated-User"); + config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Authenticated-User"); handler.init(config); - HttpServletRequest request = requestWithHeader("X-Authenticated-User", "bob"); - PdpAuthHandler.Result result = handler.authenticate(request, null); + HttpServletRequest request = requestWithHeader("X-Authenticated-User", "bob"); + PdpAuthNHandler.Result result = handler.authenticate(request, null); - assertEquals(PdpAuthHandler.Result.Status.AUTHENTICATED, result.getStatus()); + assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus()); assertEquals("bob", result.getUserName()); } 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 index 89d835d13a..891cd2bb84 100644 --- a/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java +++ b/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java @@ -29,13 +29,13 @@ public class KerberosAuthHandlerTest { @AfterEach - public void restoreDefaultRules() throws Exception { + public void restoreDefaultRules() { KerberosName.setRules("DEFAULT"); } @Test public void testApplyNameRules_usesConfiguredRules() throws Exception { - KerberosAuthHandler handler = new KerberosAuthHandler(); + KerberosAuthNHandler handler = new KerberosAuthNHandler(); invokePrivateVoid(handler, "initializeKerberosNameRules", new Class[] {String.class}, "DEFAULT"); @@ -46,7 +46,7 @@ public void testApplyNameRules_usesConfiguredRules() throws Exception { @Test public void testApplyNameRules_fallsBackForInvalidPrincipal() throws Exception { - KerberosAuthHandler handler = new KerberosAuthHandler(); + KerberosAuthNHandler handler = new KerberosAuthNHandler(); invokePrivateVoid(handler, "initializeKerberosNameRules", new Class[] {String.class}, "DEFAULT"); diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthFilterTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java similarity index 73% rename from pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthFilterTest.java rename to pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java index 41192325aa..0d0e183c55 100644 --- a/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthFilterTest.java +++ b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java @@ -19,6 +19,7 @@ package org.apache.ranger.pdp.security; +import org.apache.ranger.pdp.config.RangerPdpConstants; import org.junit.jupiter.api.Test; import javax.servlet.FilterConfig; @@ -35,37 +36,37 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -public class RangerPdpAuthFilterTest { +public class RangerPdpAuthNFilterTest { @Test public void testInit_skipsHeaderHandlerWhenDisabled() { - RangerPdpAuthFilter filter = new RangerPdpAuthFilter(); - Map params = new HashMap<>(); + RangerPdpAuthNFilter filter = new RangerPdpAuthNFilter(); + Map params = new HashMap<>(); - params.put(RangerPdpAuthFilter.PARAM_AUTH_TYPES, "header"); - params.put(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_ENABLED, "false"); + 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 { - RangerPdpAuthFilter filter = new RangerPdpAuthFilter(); - Map params = new HashMap<>(); + RangerPdpAuthNFilter filter = new RangerPdpAuthNFilter(); + Map params = new HashMap<>(); - params.put(RangerPdpAuthFilter.PARAM_AUTH_TYPES, "header"); - params.put(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_ENABLED, "true"); + params.put(RangerPdpConstants.PROP_AUTHN_TYPES, "header"); + params.put(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, "true"); filter.init(new TestFilterConfig(params)); - Field handlersField = RangerPdpAuthFilter.class.getDeclaredField("handlers"); + Field handlersField = RangerPdpAuthNFilter.class.getDeclaredField("handlers"); handlersField.setAccessible(true); @SuppressWarnings("unchecked") - List handlers = (List) handlersField.get(filter); + List handlers = (List) handlersField.get(filter); assertEquals(1, handlers.size()); - assertEquals(HttpHeaderAuthHandler.class, handlers.get(0).getClass()); + assertEquals(HttpHeaderAuthNHandler.class, handlers.get(0).getClass()); } private static final class TestFilterConfig implements FilterConfig { 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 0000000000..c7abce92b2 --- /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 0000000000..705deb9caa --- /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() From 2cba769339c39aa9365d049c229dd2dca0e466d4 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sat, 28 Mar 2026 19:20:33 -0700 Subject: [PATCH 07/10] RANGER-5371: addressed review comments --- .../scripts/pdp/ranger-pdp-site.xml | 4 +-- .../main/python/apache_ranger/exceptions.py | 20 ++++++------- .../ranger/pdp/config/RangerPdpConfig.java | 4 +-- .../ranger/pdp/config/RangerPdpConstants.java | 2 +- .../apache/ranger/pdp/rest/RangerPdpREST.java | 2 +- .../pdp/security/KerberosAuthNHandler.java | 6 ++-- .../pdp/security/RangerPdpAuthNFilter.java | 28 ++++++++++--------- 7 files changed, 34 insertions(+), 32 deletions(-) diff --git a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml index 2e94310ad1..3fc4e6245e 100644 --- a/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml +++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml @@ -103,10 +103,10 @@ header,jwt,kerberos - + ranger.pdp.authn.header.enabled - true + false diff --git a/intg/src/main/python/apache_ranger/exceptions.py b/intg/src/main/python/apache_ranger/exceptions.py index 86785c35fb..347468a831 100644 --- a/intg/src/main/python/apache_ranger/exceptions.py +++ b/intg/src/main/python/apache_ranger/exceptions.py @@ -35,19 +35,19 @@ def __init__(self, api, response): if api is not None and response is not None: if response.content: - try: - respJson = response.json() + 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 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 + 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 ] + except Exception: + self.msgDesc = response.content + self.messageList = [ response.content ] self.statusCode = response.status_code 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 index 749085555b..11aea39ea8 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java @@ -42,8 +42,8 @@ * *

    Authentication property names: *

      - *
    • Kerberos/SPNEGO: {@code ranger.pdp.kerberos.spnego.*} - *
    • JWT bearer token: {@code ranger.pdp.jwt.*} + *
    • Kerberos/SPNEGO: {@code ranger.pdp.authn.kerberos.*} + *
    • JWT bearer token: {@code ranger.pdp.authn.jwt.*} *
    • HTTP header: {@code ranger.pdp.authn.header.*} *
    */ 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 index aa5d4203ba..c1a4313514 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java @@ -31,7 +31,7 @@ private RangerPdpConstants() { // Request attributes set by auth filter public static final String ATTR_AUTHENTICATED_USER = "ranger.pdp.authenticated.user"; - public static final String ATTR_AUTH_TYPE = "ranger.pdp.auth.type"; + public static final String ATTR_AUTHN_TYPE = "ranger.pdp.authn.type"; // Server public static final String PROP_CONF_DIR = "ranger.pdp.conf.dir"; 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 index a83b9cfea8..2adceffba7 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -75,7 +75,7 @@ * REST resource that exposes the three core {@link RangerAuthorizer} methods over HTTP. * *

    All endpoints are under {@code /authz/v1} and produce/consume {@code application/json}. - * Authentication is enforced upstream by {@link RangerPdpAuthFilter}; the authenticated + * 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. * 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 index 76b3a67f21..c15eab1635 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java @@ -54,9 +54,9 @@ *

    Uses the JDK's built-in GSSAPI/JGSS support – no external Kerberos library is required. * The service principal and keytab must be configured via: *

      - *
    • {@code ranger.pdp.kerberos.spnego.principal} – e.g. {@code HTTP/host.example.com@REALM} - *
    • {@code ranger.pdp.kerberos.spnego.keytab} – absolute path to the keytab file - *
    • {@code hadoop.security.auth_to_local} – Hadoop-style name rules (default: {@code DEFAULT}) + *
    • {@code ranger.pdp.authn.kerberos.spnego.principal} – e.g. {@code HTTP/host.example.com@REALM} + *
    • {@code ranger.pdp.authn.kerberos.spnego.keytab} – absolute path to the keytab file + *
    • {@code ranger.pdp.authn.kerberos.name.rules} – Hadoop-style name rules (default: {@code DEFAULT}) *
    * *

    Authentication flow: 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 index 98676ba3b6..fa1ff802ae 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java @@ -43,7 +43,7 @@ /** * Servlet filter that enforces authentication for all PDP REST endpoints. * - *

    Handlers are configured via the {@code ranger.pdp.auth.types} filter init parameter + *

    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. * @@ -64,21 +64,23 @@ public void init(FilterConfig filterConfig) throws ServletException { Properties config = toProperties(filterConfig); String authnTypes = filterConfig.getInitParameter(PROP_AUTHN_TYPES); - for (String authnType : authnTypes.split(",")) { - PdpAuthNHandler handler = createHandler(authnType.trim().toLowerCase(), filterConfig); + if (StringUtils.isNotBlank(authnTypes)) { + for (String authnType : authnTypes.split(",")) { + PdpAuthNHandler handler = createHandler(authnType.trim().toLowerCase(), filterConfig); - if (handler == null) { - continue; - } + if (handler == null) { + continue; + } - try { - handler.init(config); + try { + handler.init(config); - handlers.add(handler); + handlers.add(handler); - LOG.info("{}: successfully registered authentication handler", authnType); - } catch (Exception excp) { - LOG.error("{}: failed to initialize authentication handler. Handler disabled", authnType, excp); + LOG.info("{}: successfully registered authentication handler", authnType); + } catch (Exception excp) { + LOG.error("{}: failed to initialize authentication handler. Handler disabled", authnType, excp); + } } } @@ -98,7 +100,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha switch (result.getStatus()) { case AUTHENTICATED: httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER, result.getUserName()); - httpReq.setAttribute(RangerPdpConstants.ATTR_AUTH_TYPE, result.getAuthType()); + httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHN_TYPE, result.getAuthType()); LOG.debug("doFilter(): authenticated user={}, type={}", result.getUserName(), result.getAuthType()); From ffe01ea4d93d7f8c022e0a8430247f6fb5a64843 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 29 Mar 2026 00:15:49 -0700 Subject: [PATCH 08/10] RANGER-5371: fix stats handling of authN failures --- pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java | 4 +++- .../main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java | 2 +- .../test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java | 6 +++--- .../org/apache/ranger/pdp/RangerPdpStatusServletTest.java | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java index 41e56bec1b..c11c42d8ef 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java @@ -76,8 +76,10 @@ public void recordRequestError(long elapsedNanos) { totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos)); } - public void recordAuthFailure() { + public void recordAuthFailure(long elapsedNanos) { + totalRequests.incrementAndGet(); totalAuthFailures.incrementAndGet(); + totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos)); } public long getTotalRequests() { 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 index 2adceffba7..55c7e5f125 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -406,7 +406,7 @@ private void recordRequestMetrics(Response ret, long startNanos, HttpServletRequ if (status >= 200 && status < 300) { state.recordRequestSuccess(elapsed); } else if (status == 401 || status == 403) { // UNAUTHORIZED or FORBIDDEN - state.recordAuthFailure(); + state.recordAuthFailure(elapsed); } else if (status == 400) { state.recordRequestBadRequest(elapsed); } else { diff --git a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java index 7300e96fae..5ed3cadd35 100644 --- a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java +++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java @@ -31,14 +31,14 @@ public void testRequestCountersAndAverageLatency() { stats.recordRequestSuccess(5_000_000L); // 5 ms stats.recordRequestBadRequest(15_000_000L); // 15 ms stats.recordRequestError(10_000_000L); // 10 ms - stats.recordAuthFailure(); + stats.recordAuthFailure(10_000_000L); - assertEquals(3L, stats.getTotalRequests()); + assertEquals(4L, stats.getTotalRequests()); assertEquals(1L, stats.getTotalAuthzSuccess()); assertEquals(1L, stats.getTotalAuthzBadRequest()); assertEquals(1L, stats.getTotalAuthzErrors()); assertEquals(1L, stats.getTotalAuthFailures()); - assertEquals(30_000_000L, stats.getTotalLatencyNanos()); + assertEquals(40_000_000L, stats.getTotalLatencyNanos()); assertEquals(10L, 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 index 74f4c95724..ae289c0982 100644 --- a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java +++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java @@ -52,7 +52,7 @@ public void testMetricsEndpointRendersCounters() throws Exception { stats.recordRequestSuccess(5_000_000L); stats.recordRequestError(5_000_000L); - stats.recordAuthFailure(); + stats.recordAuthFailure(5_000_000L); RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(stats, new RangerPdpConfig(), RangerPdpStatusServlet.Mode.METRICS); HttpServletRequest req = proxy(HttpServletRequest.class, (proxy, method, args) -> null); @@ -62,7 +62,7 @@ public void testMetricsEndpointRendersCounters() throws Exception { servlet.doGet(req, resp); assertEquals(HttpServletResponse.SC_OK, capture.status); - assertTrue(capture.body.toString().contains("ranger_pdp_requests_total 2")); + assertTrue(capture.body.toString().contains("ranger_pdp_requests_total 3")); assertTrue(capture.body.toString().contains("ranger_pdp_auth_failures_total 1")); } From 442198431f279eb8ec5cd2056d5ca44d7d29c4a8 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 29 Mar 2026 09:30:47 -0700 Subject: [PATCH 09/10] RANGER-5371: addressed review suggestions --- intg/src/main/python/README.md | 16 ++++--- pdp/conf.dist/logback.xml | 2 +- .../apache/ranger/pdp/RangerPdpServer.java | 2 +- .../ranger/pdp/RangerPdpStatusServlet.java | 5 ++- .../ranger/pdp/model/ErrorResponse.java | 45 +++++++++++++++++++ .../apache/ranger/pdp/rest/RangerPdpREST.java | 23 +--------- .../pdp/security/KerberosAuthNHandler.java | 2 +- .../pdp/security/RangerPdpAuthNFilter.java | 8 ++-- 8 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java diff --git a/intg/src/main/python/README.md b/intg/src/main/python/README.md index d3ea7cb04e..a8c0c26710 100644 --- a/intg/src/main/python/README.md +++ b/intg/src/main/python/README.md @@ -166,13 +166,14 @@ from apache_ranger.model.ranger_authz import ( pdp = RangerPDPClient("http://localhost:6500", HTTPKerberosAuth()) req = RangerAuthzRequest({ - "requestId": "req-1", - "user": RangerUserInfo({"name": "alice"}), - "access": RangerAccessInfo({ - "resource": RangerResourceInfo({"name": "table:default/test_tbl1"}), - "permissions": ["select"] + '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"}) + 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) }) res = pdp.authorize(req) @@ -193,7 +194,8 @@ payload = { "requestId": "req-1", "user": {"name": "alice"}, "access": { - "resource": {"name": "table:default/test_tbl1"}, + "resource": {"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"]}, + "action": "QUERY", "permissions": ["select"] }, "context": {"serviceType": "hive", "serviceName": "dev_hive"} diff --git a/pdp/conf.dist/logback.xml b/pdp/conf.dist/logback.xml index 90b82451a9..a52ffde14b 100644 --- a/pdp/conf.dist/logback.xml +++ b/pdp/conf.dist/logback.xml @@ -18,7 +18,7 @@ - System.out + System.out %d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java index c946caef79..959426b5c0 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java @@ -186,7 +186,7 @@ private Connector createConnector() { connector.setProperty("truststoreFile", config.getTruststoreFile()); connector.setProperty("truststorePass", config.getTruststorePassword()); connector.setProperty("truststoreType", config.getTruststoreType()); - connector.setProperty("clientAuth", "want"); + connector.setProperty("clientAuth", "true"); } } diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java index 6471c59eb9..e87207e2f3 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java @@ -27,6 +27,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; import java.io.File; import java.io.IOException; @@ -73,7 +74,7 @@ private void writeLive(HttpServletResponse resp) throws IOException { payload.put("live", runtimeState.isServerStarted()); resp.setStatus(runtimeState.isServerStarted() ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE); - resp.setContentType("application/json"); + resp.setContentType(MediaType.APPLICATION_JSON); MAPPER.writeValue(resp.getOutputStream(), payload); } @@ -91,7 +92,7 @@ private void writeReady(HttpServletResponse resp) throws IOException { payload.put("policyCacheAgeMs", getPolicyCacheAgeMs()); resp.setStatus(ready ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE); - resp.setContentType("application/json"); + resp.setContentType(MediaType.APPLICATION_JSON); MAPPER.writeValue(resp.getOutputStream(), payload); } 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 0000000000..d06c1e6708 --- /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/RangerPdpREST.java b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java index 55c7e5f125..e37f0c955e 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -19,8 +19,6 @@ package org.apache.ranger.pdp.rest; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.JsonInclude; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; @@ -38,6 +36,7 @@ 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; @@ -484,24 +483,4 @@ private Response serverError() { .entity(new ErrorResponse(INTERNAL_SERVER_ERROR, "Internal Server Error")) .build(); } - - @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) - @JsonInclude(JsonInclude.Include.NON_NULL) - public static 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/security/KerberosAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java index c15eab1635..e46e3e5d38 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java @@ -118,7 +118,7 @@ public void init(Properties config) throws Exception { serverCred = Subject.doAs(serviceSubject, (PrivilegedExceptionAction) () -> gssManager.createCredential(serverName, tokenLifetime, new Oid[] {SPNEGO_OID, KRB5_OID}, GSSCredential.ACCEPT_ONLY)); - LOG.info("KerberosAuthHandler initialized; principal={} (bound acceptor credential to configured principal)", principal); + LOG.info("KerberosAuthNHandler initialized; principal={} (bound acceptor credential to configured principal)", principal); } @Override 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 index fa1ff802ae..32d04ef0a4 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java @@ -32,6 +32,8 @@ 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; @@ -138,9 +140,9 @@ private void sendUnauthenticated(HttpServletResponse response) throws IOExceptio } } - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.getWriter().write("{\"code\":\"UNAUTHENTICATED\",\"message\":\"Authentication required\"}"); + 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) { From bf4f4ae459d7d6abc5d1d1f608b1fa6d3745efd3 Mon Sep 17 00:00:00 2001 From: Madhan Neethiraj Date: Sun, 29 Mar 2026 12:38:51 -0700 Subject: [PATCH 10/10] RANGER-5371: addressed review comments --- .../embedded/RangerEmbeddedAuthorizer.java | 6 +++ .../apache/ranger/pdp/RangerPdpServer.java | 4 +- .../ranger/pdp/RangerPdpStatusServlet.java | 47 ++++++------------- .../apache/ranger/pdp/rest/RangerPdpREST.java | 11 +++-- .../pdp/RangerPdpStatusServletTest.java | 13 ++--- 5 files changed, 36 insertions(+), 45 deletions(-) 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 365255d5b4..9075b92344 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.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; +import java.util.Set; 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; @@ -131,6 +133,10 @@ public RangerMultiAuthzResult authorize(RangerMultiAuthzRequest request, RangerA return authorize(request, plugin, auditHandler); } + public Set getLoadedServices() { + return Collections.unmodifiableSet(this.plugins.keySet()); + } + @Override protected void validateAccessContext(RangerAccessContext context) throws RangerAuthzException { super.validateAccessContext(context); diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java index 959426b5c0..711ce06a1c 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java @@ -110,10 +110,12 @@ public void stop() { } private void initAuthorizer() throws RangerAuthzException { - authorizer = new RangerEmbeddedAuthorizer(config.getAuthzProperties()); + RangerAuthorizer authorizer = new RangerEmbeddedAuthorizer(config.getAuthzProperties()); authorizer.init(); + this.authorizer = authorizer; + runtimeStats.setAuthorizerInitialized(true); LOG.info("RangerEmbeddedAuthorizer initialised"); diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java index e87207e2f3..c347817ebd 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java @@ -20,19 +20,20 @@ package org.apache.ranger.pdp; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.StringUtils; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; import org.apache.ranger.pdp.config.RangerPdpConfig; 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.File; 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(); @@ -56,10 +57,10 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO writeLive(resp); break; case READY: - writeReady(resp); + writeReady(req, resp); break; case METRICS: - writeMetrics(resp); + writeMetrics(req, resp); break; default: resp.setStatus(HttpServletResponse.SC_NOT_FOUND); @@ -79,7 +80,7 @@ private void writeLive(HttpServletResponse resp) throws IOException { MAPPER.writeValue(resp.getOutputStream(), payload); } - private void writeReady(HttpServletResponse resp) throws IOException { + private void writeReady(HttpServletRequest req, HttpServletResponse resp) throws IOException { boolean ready = runtimeState.isServerStarted() && runtimeState.isAuthorizerInitialized() && runtimeState.isAcceptingRequests(); Map payload = new LinkedHashMap<>(); @@ -89,7 +90,7 @@ private void writeReady(HttpServletResponse resp) throws IOException { payload.put("ready", ready); payload.put("authorizerInitialized", runtimeState.isAuthorizerInitialized()); payload.put("acceptingRequests", runtimeState.isAcceptingRequests()); - payload.put("policyCacheAgeMs", getPolicyCacheAgeMs()); + payload.put("loadedServicesCount", getLoadedServicesCount(req)); resp.setStatus(ready ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE); resp.setContentType(MediaType.APPLICATION_JSON); @@ -97,7 +98,7 @@ private void writeReady(HttpServletResponse resp) throws IOException { MAPPER.writeValue(resp.getOutputStream(), payload); } - private void writeMetrics(HttpServletResponse resp) throws IOException { + private void writeMetrics(HttpServletRequest req, HttpServletResponse resp) throws IOException { StringBuilder sb = new StringBuilder(512); sb.append("# TYPE ranger_pdp_requests_total counter\n"); @@ -112,8 +113,8 @@ private void writeMetrics(HttpServletResponse resp) throws IOException { 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_policy_cache_age_ms gauge\n"); - sb.append("ranger_pdp_policy_cache_age_ms ").append(getPolicyCacheAgeMs()).append('\n'); + sb.append("# TYPE ranger_pdp_loaded_services_count counter\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"); @@ -121,29 +122,11 @@ private void writeMetrics(HttpServletResponse resp) throws IOException { resp.getWriter().write(sb.toString()); } - private long getPolicyCacheAgeMs() { - String cacheDir = config.get(RangerPdpConstants.PROP_AUTHZ_POLICY_CACHE_DIR, null); + private int getLoadedServicesCount(HttpServletRequest req) { + ServletContext context = req.getServletContext(); + Object authorizer = context != null ? context.getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_AUTHORIZER) : null; + Set services = authorizer instanceof RangerEmbeddedAuthorizer ? ((RangerEmbeddedAuthorizer) authorizer).getLoadedServices() : null; - if (StringUtils.isNotBlank(cacheDir)) { - File dir = new File(cacheDir); - - if (dir.exists() && dir.isDirectory()) { - File[] files = dir.listFiles((d, name) -> name.endsWith(".json")); - - if (files != null && files.length > 0) { - long latestMtime = 0L; - - for (File f : files) { - latestMtime = Math.max(latestMtime, f.lastModified()); - } - - if (latestMtime > 0L) { - return Math.max(0L, System.currentTimeMillis() - latestMtime); - } - } - } - } - - return -1; + return services != null ? services.size() : 0; } } 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 index e37f0c955e..1f447f706e 100644 --- a/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -64,6 +64,7 @@ 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; @@ -137,7 +138,7 @@ public Response authorize(RangerAuthzRequest request, @Context HttpServletReques ret = validateCaller(caller, user, access, serviceName); - if (RESPONSE_OK.equals(ret)) { + if (isStatusOk(ret)) { try { RangerAuthzResult result = authorizer.authorize(request); @@ -184,7 +185,7 @@ public Response authorizeMulti(RangerMultiAuthzRequest request, @Context HttpSer ret = validateCaller(caller, user, accesses, serviceName); - if (RESPONSE_OK.equals(ret)) { + if (isStatusOk(ret)) { try { RangerMultiAuthzResult result = authorizer.authorize(request); @@ -228,7 +229,7 @@ public Response getResourcePermissions(RangerResourcePermissionsRequest request, ret = validateCaller(caller, serviceName); - if (RESPONSE_OK.equals(ret)) { + if (isStatusOk(ret)) { try { RangerResourcePermissions result = authorizer.getResourcePermissions(request); @@ -483,4 +484,8 @@ private 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/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java index ae289c0982..c1b8533066 100644 --- a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java +++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java @@ -67,19 +67,14 @@ public void testMetricsEndpointRendersCounters() throws Exception { } @Test - public void testPolicyCacheAgeMsUsesConfiguredDirectory(@TempDir Path tempDir) throws Exception { - File cacheJson = tempDir.resolve("policy_cache.json").toFile(); - - assertTrue(cacheJson.createNewFile()); - - System.setProperty(RangerPdpConstants.PROP_AUTHZ_POLICY_CACHE_DIR, tempDir.toString()); - + public void testLoadedServicesCount() throws Exception { RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(new RangerPdpStats(), new RangerPdpConfig(), RangerPdpStatusServlet.Mode.READY); - Method method = RangerPdpStatusServlet.class.getDeclaredMethod("getPolicyCacheAgeMs"); + HttpServletRequest req = proxy(HttpServletRequest.class, (proxy, method, args) -> null); + Method method = RangerPdpStatusServlet.class.getDeclaredMethod("getLoadedServicesCount", HttpServletRequest.class); method.setAccessible(true); - long ageMs = (Long) method.invoke(servlet); + Integer ageMs = (Integer) method.invoke(servlet, req); assertTrue(ageMs >= 0L); }