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..cf5af603c9 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 @@ -37,8 +37,10 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Properties; +import java.util.Set; import 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 new HashSet<>(this.plugins.keySet()); + } + @Override protected void validateAccessContext(RangerAccessContext context) throws RangerAuthzException { super.validateAccessContext(context); @@ -218,7 +224,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..0f74b31621 --- /dev/null +++ b/dev-support/ranger-docker/Dockerfile.ranger-pdp @@ -0,0 +1,36 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ARG RANGER_BASE_IMAGE=apache/ranger-base +ARG RANGER_BASE_VERSION=20260123-2-8 + +FROM ${RANGER_BASE_IMAGE}:${RANGER_BASE_VERSION} + +ARG PDP_VERSION + +COPY ./dist/ranger-${PDP_VERSION}-pdp.tar.gz /home/ranger/dist/ +COPY ./scripts/pdp/ranger-pdp.sh ${RANGER_SCRIPTS}/ + +RUN tar xvfz /home/ranger/dist/ranger-${PDP_VERSION}-pdp.tar.gz --directory=${RANGER_HOME} \ + && ln -s ${RANGER_HOME}/ranger-${PDP_VERSION}-pdp ${RANGER_HOME}/pdp \ + && rm -f /home/ranger/dist/ranger-${PDP_VERSION}-pdp.tar.gz \ + && mkdir -p /var/log/ranger/pdp /var/run/ranger /etc/ranger/cache \ + && ln -s ${RANGER_HOME}/pdp/ranger-pdp-services.sh /usr/bin/ranger-pdp-services.sh \ + && chown -R ranger:ranger ${RANGER_HOME}/pdp/ ${RANGER_SCRIPTS}/ /var/log/ranger/ /var/run/ranger /etc/ranger/ \ + && chmod 744 ${RANGER_SCRIPTS}/ranger-pdp.sh + +USER ranger +ENTRYPOINT [ "/home/ranger/scripts/ranger-pdp.sh" ] diff --git a/dev-support/ranger-docker/README.md b/dev-support/ranger-docker/README.md index 63f2068305..b0156d61c7 100644 --- a/dev-support/ranger-docker/README.md +++ b/dev-support/ranger-docker/README.md @@ -74,14 +74,14 @@ cd dev-support/ranger-docker ### Run Ranger Services in Containers -#### Bring up ranger-core services: ranger, usersync, tagsync and ranger-kms in containers +#### Bring up ranger-core services: ranger, usersync, tagsync, pdp and kms in containers ~~~ # To enable file based sync source for usersync do: # export ENABLE_FILE_SYNC_SOURCE=true # valid values for RANGER_DB_TYPE: mysql/postgres/oracle -docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-kms.yml up -d +docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-pdp.yml -f docker-compose.ranger-kms.yml up -d # Ranger Admin can be accessed at http://localhost:6080 (admin/rangerR0cks!) ~~~ @@ -111,7 +111,7 @@ Similarly, check the `depends` section of the `docker-compose.ranger-service.yam #### Bring up all containers ~~~ ./scripts/ozone/ozone-plugin-docker-setup.sh -docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-kms.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-hbase.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-hive.yml -f docker-compose.ranger-knox.yml -f docker-compose.ranger-ozone.yml up -d +docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.yml -f docker-compose.ranger-tagsync.yml -f docker-compose.ranger-pdp.yml -f docker-compose.ranger-kms.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-hbase.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-hive.yml -f docker-compose.ranger-knox.yml -f docker-compose.ranger-ozone.yml up -d ~~~ #### To rebuild specific images and start containers with the new image: @@ -122,4 +122,4 @@ docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-usersync.ym #### To bring up audit server, solr and hdfs consumer. Make sure kafka,solr and hdfs containers are running before bring up audit server. ~~~ docker compose -f docker-compose.ranger.yml -f docker-compose.ranger-hadoop.yml -f docker-compose.ranger-kafka.yml -f docker-compose.ranger-audit-server.yml up -d -~~~ \ No newline at end of file +~~~ diff --git a/dev-support/ranger-docker/docker-compose.ranger-pdp.yml b/dev-support/ranger-docker/docker-compose.ranger-pdp.yml new file mode 100644 index 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..3fc4e6245e --- /dev/null +++ b/dev-support/ranger-docker/scripts/pdp/ranger-pdp-site.xml @@ -0,0 +1,240 @@ + + + + + + + ranger.pdp.port + 6500 + + + + ranger.pdp.log.dir + /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 + 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.authn.types + header,jwt,kerberos + + + + + ranger.pdp.authn.header.enabled + false + + + + ranger.pdp.authn.header.username + X-Forwarded-User + + + + + 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 + + + + ranger.pdp.authn.kerberos.spnego.keytab + /etc/keytabs/HTTP.keytab + + + + ranger.pdp.authn.kerberos.name.rules + DEFAULT + + + + + ranger.authz.default.policy.rest.url + http://ranger: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: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 + HTTP + + + + 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..a8c0c26710 100644 --- a/intg/src/main/python/README.md +++ b/intg/src/main/python/README.md @@ -39,6 +39,173 @@ Package Version apache-ranger 0.0.12 ``` +## Python API clients + +### Ranger Admin (`RangerClient`) + +Base URL: `http(s)://:` + +`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', 'subResources': ['column:id', 'column:name', 'column:email']}), + 'action': 'QUERY', + 'permissions': ['select'] + }), + 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) +}) + +res = pdp.authorize(req) +print(res.decision) +``` + +Raw REST example using `requests` (JWT bearer): + +```python +import requests + +url = "http://localhost:6500/authz/v1/authorize" +headers = { + "Authorization": "Bearer ", + "Content-Type": "application/json" +} +payload = { + "requestId": "req-1", + "user": {"name": "alice"}, + "access": { + "resource": {"name": "table:default/test_tbl1", "subResources": ["column:id", "column:name", "column:email"]}, + "action": "QUERY", + "permissions": ["select"] + }, + "context": {"serviceType": "hive", "serviceName": "dev_hive"} +} + +resp = requests.post(url, headers=headers, json=payload, timeout=30) +resp.raise_for_status() +print(resp.json()) +``` + ## Usage ```python test_ranger.py``` @@ -122,113 +289,6 @@ print(' deleted service: id=' + str(created_service.id)) ``` -```python test_ranger_kms.py``` -```python -# test_ranger_kms.py -from apache_ranger.client.ranger_kms_client import RangerKMSClient -from apache_ranger.client.ranger_client import HadoopSimpleAuth -from apache_ranger.model.ranger_kms import RangerKey -import time - - -## -## Step 1: create a client to connect to Apache Ranger KMS -## -kms_url = 'http://localhost:9292' -kms_auth = HadoopSimpleAuth('keyadmin') - -# For Kerberos authentication -# -# from requests_kerberos import HTTPKerberosAuth -# -# kms_auth = HTTPKerberosAuth() -# -# For HTTP Basic authentication -# -# kms_auth = ('keyadmin', 'rangerR0cks!') - -kms_client = RangerKMSClient(kms_url, kms_auth) - - - -## -## Step 2: Let's call KMS APIs -## - -kms_status = kms_client.kms_status() -print('kms_status():', kms_status) -print() - -key_name = 'test_' + str(int(time.time() * 1000)) - -key = kms_client.create_key(RangerKey({'name':key_name})) -print('create_key(' + key_name + '):', key) -print() - -rollover_key = kms_client.rollover_key(key_name, key.material) -print('rollover_key(' + key_name + '):', rollover_key) -print() - -kms_client.invalidate_cache_for_key(key_name) -print('invalidate_cache_for_key(' + key_name + ')') -print() - -key_metadata = kms_client.get_key_metadata(key_name) -print('get_key_metadata(' + key_name + '):', key_metadata) -print() - -current_key = kms_client.get_current_key(key_name) -print('get_current_key(' + key_name + '):', current_key) -print() - -encrypted_keys = kms_client.generate_encrypted_key(key_name, 6) -print('generate_encrypted_key(' + key_name + ', ' + str(6) + '):') -for i in range(len(encrypted_keys)): - encrypted_key = encrypted_keys[i] - decrypted_key = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material) - reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material) - print(' encrypted_keys[' + str(i) + ']: ', encrypted_key) - print(' decrypted_key[' + str(i) + ']: ', decrypted_key) - print(' reencrypted_key[' + str(i) + ']:', reencrypted_key) -print() - -reencrypted_keys = kms_client.batch_reencrypt_encrypted_keys(key_name, encrypted_keys) -print('batch_reencrypt_encrypted_keys(' + key_name + ', ' + str(len(encrypted_keys)) + '):') -for i in range(len(reencrypted_keys)): - print(' batch_reencrypt_encrypted_key[' + str(i) + ']:', reencrypted_keys[i]) -print() - -key_versions = kms_client.get_key_versions(key_name) -print('get_key_versions(' + key_name + '):', len(key_versions)) -for i in range(len(key_versions)): - print(' key_versions[' + str(i) + ']:', key_versions[i]) -print() - -for i in range(len(key_versions)): - key_version = kms_client.get_key_version(key_versions[i].versionName) - print('get_key_version(' + str(i) + '):', key_version) -print() - -key_names = kms_client.get_key_names() -print('get_key_names():', len(key_names)) -for i in range(len(key_names)): - print(' key_name[' + str(i) + ']:', key_names[i]) -print() - -keys_metadata = kms_client.get_keys_metadata(key_names) -print('get_keys_metadata(' + str(key_names) + '):', len(keys_metadata)) -for i in range(len(keys_metadata)): - print(' key_metadata[' + str(i) + ']:', keys_metadata[i]) -print() - -key = kms_client.get_key(key_name) -print('get_key(' + key_name + '):', key) -print() - -kms_client.delete_key(key_name) -print('delete_key(' + key_name + ')') -``` - ```python test_ranger_user_mgmt.py``` ```python # test_ranger_user_mgmt.py @@ -365,4 +425,198 @@ print(f'\nDeleting user {user.name}') user_mgmt.delete_user_by_id(created_user.id, True) ``` -For more examples, checkout `sample-client` python project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python) module. +```python test_ranger_kms.py``` +```python +# test_ranger_kms.py +from apache_ranger.client.ranger_kms_client import RangerKMSClient +from apache_ranger.client.ranger_client import HadoopSimpleAuth +from apache_ranger.model.ranger_kms import RangerKey +import time + + +## +## Step 1: create a client to connect to Apache Ranger KMS +## +kms_url = 'http://localhost:9292' +kms_auth = HadoopSimpleAuth('keyadmin') + +# For Kerberos authentication +# +# from requests_kerberos import HTTPKerberosAuth +# +# kms_auth = HTTPKerberosAuth() +# +# For HTTP Basic authentication +# +# kms_auth = ('keyadmin', 'rangerR0cks!') + +kms_client = RangerKMSClient(kms_url, kms_auth) + + + +## +## Step 2: Let's call KMS APIs +## + +kms_status = kms_client.kms_status() +print('kms_status():', kms_status) +print() + +key_name = 'test_' + str(int(time.time() * 1000)) + +key = kms_client.create_key(RangerKey({'name':key_name})) +print('create_key(' + key_name + '):', key) +print() + +rollover_key = kms_client.rollover_key(key_name, key.material) +print('rollover_key(' + key_name + '):', rollover_key) +print() + +kms_client.invalidate_cache_for_key(key_name) +print('invalidate_cache_for_key(' + key_name + ')') +print() + +key_metadata = kms_client.get_key_metadata(key_name) +print('get_key_metadata(' + key_name + '):', key_metadata) +print() + +current_key = kms_client.get_current_key(key_name) +print('get_current_key(' + key_name + '):', current_key) +print() + +encrypted_keys = kms_client.generate_encrypted_key(key_name, 6) +print('generate_encrypted_key(' + key_name + ', ' + str(6) + '):') +for i in range(len(encrypted_keys)): + encrypted_key = encrypted_keys[i] + decrypted_key = kms_client.decrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material) + reencrypted_key = kms_client.reencrypt_encrypted_key(key_name, encrypted_key.versionName, encrypted_key.iv, encrypted_key.encryptedKeyVersion.material) + print(' encrypted_keys[' + str(i) + ']: ', encrypted_key) + print(' decrypted_key[' + str(i) + ']: ', decrypted_key) + print(' reencrypted_key[' + str(i) + ']:', reencrypted_key) +print() + +reencrypted_keys = kms_client.batch_reencrypt_encrypted_keys(key_name, encrypted_keys) +print('batch_reencrypt_encrypted_keys(' + key_name + ', ' + str(len(encrypted_keys)) + '):') +for i in range(len(reencrypted_keys)): + print(' batch_reencrypt_encrypted_key[' + str(i) + ']:', reencrypted_keys[i]) +print() + +key_versions = kms_client.get_key_versions(key_name) +print('get_key_versions(' + key_name + '):', len(key_versions)) +for i in range(len(key_versions)): + print(' key_versions[' + str(i) + ']:', key_versions[i]) +print() + +for i in range(len(key_versions)): + key_version = kms_client.get_key_version(key_versions[i].versionName) + print('get_key_version(' + str(i) + '):', key_version) +print() + +key_names = kms_client.get_key_names() +print('get_key_names():', len(key_names)) +for i in range(len(key_names)): + print(' key_name[' + str(i) + ']:', key_names[i]) +print() + +keys_metadata = kms_client.get_keys_metadata(key_names) +print('get_keys_metadata(' + str(key_names) + '):', len(keys_metadata)) +for i in range(len(keys_metadata)): + print(' key_metadata[' + str(i) + ']:', keys_metadata[i]) +print() + +key = kms_client.get_key(key_name) +print('get_key(' + key_name + '):', key) +print() + +kms_client.delete_key(key_name) +print('delete_key(' + key_name + ')') +``` + +```python test_ranger_pdp.py``` +```python +from apache_ranger.client.ranger_pdp_client import RangerPDPClient +from apache_ranger.model.ranger_authz import RangerAccessContext, RangerAccessInfo +from apache_ranger.model.ranger_authz import RangerAuthzRequest, RangerMultiAuthzRequest +from apache_ranger.model.ranger_authz import RangerResourceInfo, RangerResourcePermissionsRequest, RangerUserInfo + +## +## Step 1: create a client to connect to Ranger PDP +## +pdp_url = 'http://localhost:6500' + +# For Kerberos authentication +# +# from requests_kerberos import HTTPKerberosAuth +# +# pdp = RangerPDPClient(pdp_url, HTTPKerberosAuth()) + +# For trusted-header authN with PDP (example only): +# +pdp = RangerPDPClient(pdp_url, auth=None, headers={ 'X-Forwarded-User': 'hive' }) + +## +## Step 2: call PDP authorization APIs +## +req = RangerAuthzRequest({ + 'requestId': 'req-1', + 'user': RangerUserInfo({'name': 'alice'}), + 'access': RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}), 'permissions': ['create']}), + 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) +}) + +res = pdp.authorize(req) + +print('authorize():') +print(f' {req}') +print(f' {res}') +print('\n') + + +req = RangerAuthzRequest({ + 'requestId': 'req-2', + 'user': RangerUserInfo({'name': 'alice'}), + 'access': RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email']}), 'permissions': ['select']}), + 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) +}) + +res = pdp.authorize(req) + +print('authorize():') +print(f' {req}') +print(f' {res}') +print('\n') + + +req = RangerMultiAuthzRequest({ + 'requestId': 'req-3', + 'user': RangerUserInfo({'name': 'alice'}), + 'accesses': [ + RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_tbl1', 'subResources': ['column:id', 'column:name', 'column:email'], 'attributes': {'OWNER': 'alice'}}), 'permissions': ['select']}), + RangerAccessInfo({'resource': RangerResourceInfo({'name': 'table:default/test_vw1'}), 'permissions': ['create']}) + ], + 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) +}) + +res = pdp.authorize_multi(req) + +print('authorize_multi():') +print(f' {req}') +print(f' {res}') +print('\n') + + +req = RangerResourcePermissionsRequest({ + 'requestId': 'req-4', + 'resource': RangerResourceInfo({'name': 'table:default/test_tbl1'}), + 'context': RangerAccessContext({'serviceType': 'hive', 'serviceName': 'dev_hive'}) +}) + +res = pdp.get_resource_permissions(req) + +print('get_resource_permissions():') +print(f' {req}') +print(f' {res}') +print('\n') +``` + +For more examples, checkout `sample-client` python project in [ranger-examples](https://github.com/apache/ranger/blob/master/ranger-examples/sample-client/src/main/python) module (including `sample_client.py`, `user_mgmt.py`, `sample_kms_client.py`, and `sample_pdp_client.py`). diff --git a/intg/src/main/python/apache_ranger/client/__init__.py b/intg/src/main/python/apache_ranger/client/__init__.py index 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..9268a2f4c0 --- /dev/null +++ b/intg/src/main/python/apache_ranger/client/ranger_pdp_client.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from apache_ranger.client.ranger_client import RangerClientHttp +from apache_ranger.model.ranger_authz import RangerAuthzRequest, RangerAuthzResult +from apache_ranger.model.ranger_authz import RangerMultiAuthzRequest, RangerMultiAuthzResult +from apache_ranger.model.ranger_authz import RangerResourcePermissions, RangerResourcePermissionsRequest +from apache_ranger.utils import API, HttpMethod, HTTPStatus +from apache_ranger.utils import type_coerce + +LOG = logging.getLogger(__name__) + + +class RangerPDPClient: + """ + Python client for Ranger PDP authorization APIs. + + Base URL should point to PDP server endpoint, for example: + http://localhost:6500 + """ + + def __init__(self, url, auth, query_params=None, headers=None): + self.client_http = RangerClientHttp(url, auth, query_params, headers) + self.session = self.client_http.session + logging.getLogger("requests").setLevel(logging.WARNING) + + def authorize(self, authz_request): + """ + Call POST /authz/v1/authorize + :param authz_request: dict-like or RangerAuthzRequest + :return: RangerAuthzResult + """ + req = type_coerce(authz_request, RangerAuthzRequest) + resp = self.client_http.call_api(RangerPDPClient.AUTHORIZE, request_data=req) + return type_coerce(resp, RangerAuthzResult) + + def authorize_multi(self, multi_authz_request): + """ + Call POST /authz/v1/authorizeMulti + :param multi_authz_request: dict-like or RangerMultiAuthzRequest + :return: RangerMultiAuthzResult + """ + req = type_coerce(multi_authz_request, RangerMultiAuthzRequest) + resp = self.client_http.call_api(RangerPDPClient.AUTHORIZE_MULTI, request_data=req) + return type_coerce(resp, RangerMultiAuthzResult) + + def get_resource_permissions(self, resource_perm_request): + """ + Call POST /authz/v1/permissions + :param resource_perm_request: dict-like OR RangerResourcePermissionsRequest + :return: RangerResourcePermissions + """ + req = type_coerce(resource_perm_request, RangerResourcePermissionsRequest) + resp = self.client_http.call_api(RangerPDPClient.GET_RESOURCE_PERMISSIONS, request_data=req) + + return type_coerce(resp, RangerResourcePermissions) + + # URIs + URI_BASE = "authz/v1" + URI_AUTHORIZE = URI_BASE + "/authorize" + URI_AUTHORIZE_MULTI = URI_BASE + "/authorizeMulti" + URI_RESOURCE_PERMISSIONS = URI_BASE + "/permissions" + + # APIs + AUTHORIZE = API(URI_AUTHORIZE, HttpMethod.POST, HTTPStatus.OK) + AUTHORIZE_MULTI = API(URI_AUTHORIZE_MULTI, HttpMethod.POST, HTTPStatus.OK) + GET_RESOURCE_PERMISSIONS = API(URI_RESOURCE_PERMISSIONS, HttpMethod.POST, HTTPStatus.OK) + diff --git a/intg/src/main/python/apache_ranger/exceptions.py b/intg/src/main/python/apache_ranger/exceptions.py index 9f19a59ac8..347468a831 100644 --- a/intg/src/main/python/apache_ranger/exceptions.py +++ b/intg/src/main/python/apache_ranger/exceptions.py @@ -33,17 +33,21 @@ def __init__(self, api, response): self.msgDesc = None self.messageList = None - print(response) - if api is not None and response is not None: if response.content: - try: - respJson = response.json() - self.msgDesc = respJson['msgDesc'] if respJson is not None and 'msgDesc' in respJson else None - self.messageList = respJson['messageList'] if respJson is not None and 'messageList' in respJson else None - except Exception: - self.msgDesc = response.content - self.messageList = [ response.content ] + try: + respJson = response.json() + + if respJson: + self.msgDesc = respJson['msgDesc'] if 'msgDesc' in respJson else None + self.messageList = respJson['messageList'] if 'messageList' in respJson else None + + if self.msgDesc is None: + self.msgDesc = respJson['message'] if 'message' in respJson else None + + except Exception: + self.msgDesc = response.content + self.messageList = [ response.content ] self.statusCode = response.status_code diff --git a/intg/src/main/python/apache_ranger/model/__init__.py b/intg/src/main/python/apache_ranger/model/__init__.py index 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..ca4b906132 --- /dev/null +++ b/intg/src/main/python/apache_ranger/model/ranger_authz.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python + +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from apache_ranger.model.ranger_base import RangerBase +from apache_ranger.utils import non_null, type_coerce, type_coerce_dict, type_coerce_list + + +class RangerUserInfo(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + + self.name = attrs.get("name") + self.attributes = attrs.get("attributes") + self.groups = attrs.get("groups") + self.roles = attrs.get("roles") + + +class RangerResourceInfo(RangerBase): + SCOPE_SELF = "SELF" + SCOPE_SELF_OR_ANY_CHILD = "SELF_OR_ANY_CHILD" + SCOPE_SELF_OR_ANY_DESCENDANT = "SELF_OR_ANY_DESCENDANT" + + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + + self.name = attrs.get("name") + self.subResources = attrs.get("subResources") + self.nameMatchScope = attrs.get("nameMatchScope") + self.attributes = attrs.get("attributes") + + +class RangerAccessInfo(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + + self.resource = attrs.get("resource") + self.action = attrs.get("action") + self.permissions = attrs.get("permissions") + + def type_coerce_attrs(self): + super(RangerAccessInfo, self).type_coerce_attrs() + self.resource = type_coerce(self.resource, RangerResourceInfo) + + +class RangerAccessContext(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + + self.serviceType = attrs.get("serviceType") + self.serviceName = attrs.get("serviceName") + self.accessTime = attrs.get("accessTime") + self.clientIpAddress = attrs.get("clientIpAddress") + self.forwardedIpAddresses = attrs.get("forwardedIpAddresses") + self.additionalInfo = attrs.get("additionalInfo") + + +class RangerAuthzRequest(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + + self.requestId = attrs.get("requestId") + self.user = attrs.get("user") + self.access = attrs.get("access") + self.context = attrs.get("context") + + def type_coerce_attrs(self): + super(RangerAuthzRequest, self).type_coerce_attrs() + self.user = type_coerce(self.user, RangerUserInfo) + self.access = type_coerce(self.access, RangerAccessInfo) + self.context = type_coerce(self.context, RangerAccessContext) + + +class RangerMultiAuthzRequest(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + + self.requestId = attrs.get("requestId") + self.user = attrs.get("user") + self.accesses = attrs.get("accesses") + self.context = attrs.get("context") + + def type_coerce_attrs(self): + super(RangerMultiAuthzRequest, self).type_coerce_attrs() + self.user = type_coerce(self.user, RangerUserInfo) + self.accesses = type_coerce_list(self.accesses, RangerAccessInfo) + self.context = type_coerce(self.context, RangerAccessContext) + + +class RangerPolicyInfo(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.id = attrs.get("id") + self.version = attrs.get("version") + + +class RangerAccessResult(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.decision = attrs.get("decision") + self.policy = attrs.get("policy") + + def type_coerce_attrs(self): + super(RangerAccessResult, self).type_coerce_attrs() + self.policy = type_coerce(self.policy, RangerPolicyInfo) + + +class RangerDataMaskResult(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.maskType = attrs.get("maskType") + self.maskedValue = attrs.get("maskedValue") + self.policy = attrs.get("policy") + + def type_coerce_attrs(self): + super(RangerDataMaskResult, self).type_coerce_attrs() + self.policy = type_coerce(self.policy, RangerPolicyInfo) + + +class RangerRowFilterResult(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.filterExpr = attrs.get("filterExpr") + self.policy = attrs.get("policy") + + def type_coerce_attrs(self): + super(RangerRowFilterResult, self).type_coerce_attrs() + self.policy = type_coerce(self.policy, RangerPolicyInfo) + + +class RangerResultInfo(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.access = attrs.get("access") + self.dataMask = attrs.get("dataMask") + self.rowFilter = attrs.get("rowFilter") + self.additionalInfo = attrs.get("additionalInfo") + + def type_coerce_attrs(self): + super(RangerResultInfo, self).type_coerce_attrs() + self.access = type_coerce(self.access, RangerAccessResult) + self.dataMask = type_coerce(self.dataMask, RangerDataMaskResult) + self.rowFilter = type_coerce(self.rowFilter, RangerRowFilterResult) + + +class RangerPermissionResult(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.permission = attrs.get("permission") + self.access = attrs.get("access") + self.dataMask = attrs.get("dataMask") + self.rowFilter = attrs.get("rowFilter") + self.additionalInfo = attrs.get("additionalInfo") + self.subResources = attrs.get("subResources") + + def type_coerce_attrs(self): + super(RangerPermissionResult, self).type_coerce_attrs() + self.access = type_coerce(self.access, RangerAccessResult) + self.dataMask = type_coerce(self.dataMask, RangerDataMaskResult) + self.rowFilter = type_coerce(self.rowFilter, RangerRowFilterResult) + self.subResources = type_coerce_dict(self.subResources, RangerResultInfo) + + +class RangerAuthzResult(RangerBase): + DECISION_ALLOW = "ALLOW" + DECISION_DENY = "DENY" + DECISION_NOT_DETERMINED = "NOT_DETERMINED" + DECISION_PARTIAL = "PARTIAL" + + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.requestId = attrs.get("requestId") + self.decision = attrs.get("decision") + self.permissions = attrs.get("permissions") + + def type_coerce_attrs(self): + super(RangerAuthzResult, self).type_coerce_attrs() + self.permissions = type_coerce_dict(self.permissions, RangerPermissionResult) + + +class RangerMultiAuthzResult(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.requestId = attrs.get("requestId") + self.decision = attrs.get("decision") + self.accesses = attrs.get("accesses") + + def type_coerce_attrs(self): + super(RangerMultiAuthzResult, self).type_coerce_attrs() + self.accesses = type_coerce_list(self.accesses, RangerAuthzResult) + + +class RangerResourcePermissionsRequest(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.requestId = attrs.get("requestId") + self.resource = attrs.get("resource") + self.context = attrs.get("context") + + def type_coerce_attrs(self): + super(RangerResourcePermissionsRequest, self).type_coerce_attrs() + self.resource = type_coerce(self.resource, RangerResourceInfo) + self.context = type_coerce(self.context, RangerAccessContext) + + +class RangerResourcePermissions(RangerBase): + def __init__(self, attrs=None): + attrs = non_null(attrs, {}) + RangerBase.__init__(self, attrs) + self.resource = attrs.get("resource") + self.users = attrs.get("users") + self.groups = attrs.get("groups") + self.roles = attrs.get("roles") + + def type_coerce_attrs(self): + super(RangerResourcePermissions, self).type_coerce_attrs() + self.resource = type_coerce(self.resource, RangerResourceInfo) + self.users = _coerce_principal_permissions(self.users) + self.groups = _coerce_principal_permissions(self.groups) + self.roles = _coerce_principal_permissions(self.roles) + + +def _coerce_principal_permissions(value): + if not isinstance(value, dict): + return None + + ret = {} + for principal, permissions in value.items(): + ret[principal] = type_coerce_dict(permissions, RangerPermissionResult) + return ret + diff --git a/pdp/conf.dist/README-k8s.md b/pdp/conf.dist/README-k8s.md new file mode 100644 index 0000000000..b630bec7d0 --- /dev/null +++ b/pdp/conf.dist/README-k8s.md @@ -0,0 +1,103 @@ + +# Ranger PDP Kubernetes Notes + +This document captures Kubernetes-oriented runtime recommendations for `ranger-pdp`. + +## Health Probes + +Use: + +- Liveness: `GET /health/live` +- Readiness: `GET /health/ready` + +The readiness endpoint reports NOT_READY until: + +- server is started +- authorizer is initialized +- PDP is accepting requests + +## Metrics + +PDP exposes Prometheus-style text metrics at: + +- `GET /metrics` + +Current metrics include request counts, auth failures, average latency and loaded services count. + +## Runtime Tuning + +Thread/connection controls can be set in `ranger-pdp-site.xml` or via `JAVA_OPTS -D...`: + +- `ranger.pdp.http.connector.maxThreads` +- `ranger.pdp.http.connector.minSpareThreads` +- `ranger.pdp.http.connector.acceptCount` +- `ranger.pdp.http.connector.maxConnections` + +## Logging + +For container-native logging, logback can emit to stdout. +If file logging is needed, pass: + +`-Dlogdir=/path/to/log/dir` + +and use a file appender that resolves `${logdir}`. + +## Security Context + +Recommended: + +- run as non-root user +- readOnlyRootFilesystem: true +- mount writable volumes only for required paths (cache/log/temp if file logging is enabled) + +## Config/Secrets + +Recommended: + +- `ranger-pdp-site.xml` from ConfigMap +- keytabs/JWT keys/credentials from Secrets + +## Authentication Config Keys + +When configuring inbound PDP authentication in `ranger-pdp-site.xml`, use the +`ranger.pdp.authn.*` property names: + +- `ranger.pdp.authn.types` +- `ranger.pdp.authn.header.enabled` +- `ranger.pdp.authn.header.username` +- `ranger.pdp.authn.jwt.enabled` +- `ranger.pdp.authn.jwt.provider.url` +- `ranger.pdp.authn.jwt.public.key` +- `ranger.pdp.authn.jwt.cookie.name` +- `ranger.pdp.authn.jwt.audiences` +- `ranger.pdp.authn.kerberos.enabled` +- `ranger.pdp.authn.kerberos.spnego.principal` +- `ranger.pdp.authn.kerberos.spnego.keytab` +- `ranger.pdp.authn.kerberos.token.valid.seconds` +- `ranger.pdp.authn.kerberos.name.rules` + +## Network Policy + +Allow egress only to dependencies: + +- Ranger Admin +- Solr (if audit destination enabled) +- KDC (if Kerberos is used) + diff --git a/pdp/conf.dist/logback.xml b/pdp/conf.dist/logback.xml new file mode 100644 index 0000000000..a52ffde14b --- /dev/null +++ b/pdp/conf.dist/logback.xml @@ -0,0 +1,36 @@ + + + + + + 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..ddf2ff9346 --- /dev/null +++ b/pdp/conf.dist/ranger-pdp-site.xml @@ -0,0 +1,364 @@ + + + + + + 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.authn.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.authn.jwt.enabled + false + Enable JWT authentication. + + + + ranger.pdp.authn.jwt.provider.url + + URL of the JWT provider (used to fetch the public key). + + + + ranger.pdp.authn.jwt.public.key + + PEM-encoded public key for verifying JWT signatures. + + + + ranger.pdp.authn.jwt.cookie.name + hadoop-jwt + Cookie name from which a JWT bearer token may be read. + + + + ranger.pdp.authn.jwt.audiences + + Comma-separated list of accepted JWT audiences. Empty means any audience is accepted. + + + + + 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. + + + + ranger.pdp.authn.kerberos.spnego.keytab + + Path to the keytab file for the SPNEGO service principal. + + + + ranger.pdp.authn.kerberos.token.valid.seconds + 3600 + Validity period (seconds) for Kerberos authentication tokens. + + + + ranger.pdp.authn.kerberos.name.rules + DEFAULT + Rules for mapping Kerberos principal names to short usernames. + + + + 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/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..d3fba53987 --- /dev/null +++ b/pdp/scripts/ranger-pdp-services.sh @@ -0,0 +1,223 @@ +#!/bin/bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if [[ -z $1 ]]; then + echo "No argument provided." + echo "Usage: $0 {start | run | stop | restart | version}" + exit 1 +fi + +action=$1 +action=$(echo "$action" | tr '[:lower:]' '[:upper:]') + +realScriptPath=$(readlink -f "$0") +realScriptDir=$(dirname "$realScriptPath") +cd "$realScriptDir" || exit 1 +cdir=$(pwd) + +ranger_pdp_max_heap_size=${RANGER_PDP_MAX_HEAP_SIZE:-1g} + +for custom_env_script in $(find "${cdir}/conf/" -name "ranger-pdp-env*" 2>/dev/null); do + if [ -f "$custom_env_script" ]; then + . "$custom_env_script" + fi +done + +if [ -z "${RANGER_PDP_PID_DIR_PATH}" ]; then + RANGER_PDP_PID_DIR_PATH=/var/run/ranger +fi + +if [ -z "${RANGER_PDP_PID_NAME}" ]; then + RANGER_PDP_PID_NAME=pdp.pid +fi + +pidf="${RANGER_PDP_PID_DIR_PATH}/${RANGER_PDP_PID_NAME}" + +if [ -z "${UNIX_PDP_USER}" ]; then + UNIX_PDP_USER=ranger +fi + +JAVA_OPTS=" ${JAVA_OPTS} -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=200m -Xmx${ranger_pdp_max_heap_size} -Xms256m" + +if [ "${action}" == "START" ]; then + + if [ -f "${cdir}/conf/java_home.sh" ]; then + . "${cdir}/conf/java_home.sh" + fi + + for custom_env_script in $(find "${cdir}/conf.dist/" -name "ranger-pdp-env*" 2>/dev/null); do + if [ -f "$custom_env_script" ]; then + . "$custom_env_script" + fi + done + + if [ "$JAVA_HOME" != "" ]; then + export PATH=$JAVA_HOME/bin:$PATH + fi + + if [ -z "${RANGER_PDP_LOG_DIR}" ]; then + RANGER_PDP_LOG_DIR=/var/log/ranger/pdp + fi + + if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then + mkdir -p "$RANGER_PDP_LOG_DIR" + chmod 755 "$RANGER_PDP_LOG_DIR" + fi + + cp="${cdir}/conf:${cdir}/dist/*:${cdir}/lib/*" + + if [ -f "$pidf" ]; then + pid=$(cat "$pidf") + if ps -p "$pid" > /dev/null 2>&1; then + echo "Ranger PDP Service is already running [pid=${pid}]" + exit 0 + else + rm -f "$pidf" + fi + fi + + if [ -z "${RANGER_PDP_CONF_DIR}" ]; then + RANGER_PDP_CONF_DIR=${cdir}/conf + fi + + mkdir -p "${RANGER_PDP_PID_DIR_PATH}" + + SLEEP_TIME_AFTER_START=5 + nohup java -Dproc_rangerpdp ${JAVA_OPTS} \ + -Dlogdir="${RANGER_PDP_LOG_DIR}" \ + -Dlogback.configurationFile="file:${RANGER_PDP_CONF_DIR}/logback.xml" \ + -Dranger.pdp.conf.dir="${RANGER_PDP_CONF_DIR}" \ + -Duser="${USER}" \ + -Dhostname="${HOSTNAME}" \ + -cp "${cp}" \ + org.apache.ranger.pdp.RangerPdpServer \ + > "${RANGER_PDP_LOG_DIR}/pdp.out" 2>&1 & + + VALUE_OF_PID=$! + echo "Starting Ranger PDP Service" + sleep $SLEEP_TIME_AFTER_START + + if ps -p $VALUE_OF_PID > /dev/null 2>&1; then + echo $VALUE_OF_PID > "${pidf}" + chown "${UNIX_PDP_USER}" "${pidf}" 2>/dev/null || true + chmod 660 "${pidf}" + pid=$(cat "$pidf") + echo "Ranger PDP Service with pid ${pid} has started." + else + echo "Ranger PDP Service failed to start!" + exit 1 + fi + exit 0 + +elif [ "${action}" == "RUN" ]; then + if [ -f "${cdir}/conf/java_home.sh" ]; then + . "${cdir}/conf/java_home.sh" + fi + + for custom_env_script in $(find "${cdir}/conf.dist/" -name "ranger-pdp-env*" 2>/dev/null); do + if [ -f "$custom_env_script" ]; then + . "$custom_env_script" + fi + done + + if [ "$JAVA_HOME" != "" ]; then + export PATH=$JAVA_HOME/bin:$PATH + fi + + if [ -z "${RANGER_PDP_LOG_DIR}" ]; then + RANGER_PDP_LOG_DIR=/var/log/ranger/pdp + fi + + if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then + mkdir -p "$RANGER_PDP_LOG_DIR" + chmod 755 "$RANGER_PDP_LOG_DIR" + fi + + if [ -z "${RANGER_PDP_CONF_DIR}" ]; then + RANGER_PDP_CONF_DIR=${cdir}/conf + fi + + cp="${cdir}/conf:${cdir}/dist/*:${cdir}/lib/*" + exec java -Dproc_rangerpdp ${JAVA_OPTS} \ + -Dlogdir="${RANGER_PDP_LOG_DIR}" \ + -Dlogback.configurationFile="file:${RANGER_PDP_CONF_DIR}/logback.xml" \ + -Dranger.pdp.conf.dir="${RANGER_PDP_CONF_DIR}" \ + -Duser="${USER}" \ + -Dhostname="${HOSTNAME}" \ + -cp "${cp}" \ + org.apache.ranger.pdp.RangerPdpServer + +elif [ "${action}" == "STOP" ]; then + + WAIT_TIME_FOR_SHUTDOWN=2 + NR_ITER_FOR_SHUTDOWN_CHECK=15 + + if [ -f "$pidf" ]; then + pid=$(cat "$pidf") + else + pid=$(ps -ef | grep java | grep -- '-Dproc_rangerpdp' | grep -v grep | awk '{ print $2 }') + if [ "$pid" != "" ]; then + echo "pid file (${pidf}) not found; taking pid from 'ps' output." + else + echo "Ranger PDP Service is not running." + exit 0 + fi + fi + + echo "Stopping Ranger PDP Service (pid=${pid})..." + kill -15 "$pid" + + for ((i=0; i /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..a3027184fe --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java @@ -0,0 +1,295 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.valves.AccessLogValve; +import org.apache.coyote.http2.Http2Protocol; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.pdp.config.RangerPdpConfig; +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.apache.ranger.pdp.rest.RangerPdpApplication; +import org.apache.ranger.pdp.security.RangerPdpAuthNFilter; +import org.apache.ranger.pdp.security.RangerPdpRequestContextFilter; +import org.apache.tomcat.util.descriptor.web.FilterDef; +import org.apache.tomcat.util.descriptor.web.FilterMap; +import org.glassfish.jersey.servlet.ServletContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Servlet; +import javax.servlet.http.HttpServlet; + +import java.io.File; + +/** + * Main entry point for the Ranger Policy Decision Point (PDP) server. + * + *

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 RangerPdpAuthNFilter} (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 { + RangerAuthorizer authorizer = new RangerEmbeddedAuthorizer(config.getAuthzProperties()); + + authorizer.init(); + + this.authorizer = authorizer; + + runtimeStats.setAuthorizerInitialized(true); + + LOG.info("RangerEmbeddedAuthorizer initialised"); + } + + private void startTomcat() throws Exception { + tomcat = new Tomcat(); + + tomcat.setConnector(createConnector()); + + String docBase = new File(System.getProperty("java.io.tmpdir"), "ranger-pdp-webapps").getAbsolutePath(); + + new File(docBase).mkdirs(); + + Context ctx = tomcat.addContext("", docBase); + + ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_AUTHORIZER, authorizer); + ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_CONFIG, config); + ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE, runtimeStats); + + addAuthFilter(ctx); + addJerseyServlet(ctx); + addStatusEndpoints(ctx); + addAccessLogValve(); + + Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "ranger-pdp-shutdown")); + + tomcat.start(); + + runtimeStats.setServerStarted(true); + runtimeStats.setAcceptingRequests(true); + + LOG.info("Ranger PDP server listening on port {} (SSL={}, HTTP/2={})", config.getPort(), config.isSslEnabled(), config.isHttp2Enabled()); + + tomcat.getServer().await(); + } + + /** + * Builds the Tomcat connector. + * + *

When SSL is enabled the connector is configured as an HTTPS endpoint. + * When HTTP/2 is enabled an {@link Http2Protocol} upgrade protocol is added, + * supporting both {@code h2} (over TLS) and {@code h2c} (cleartext upgrade) depending + * on whether SSL is enabled. + */ + private Connector createConnector() { + Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); + + connector.setPort(config.getPort()); + connector.setProperty("maxThreads", String.valueOf(config.getHttpConnectorMaxThreads())); + connector.setProperty("minSpareThreads", String.valueOf(config.getHttpConnectorMinSpareThreads())); + connector.setProperty("acceptCount", String.valueOf(config.getHttpConnectorAcceptCount())); + connector.setProperty("maxConnections", String.valueOf(config.getHttpConnectorMaxConnections())); + + LOG.info("Configured HTTP connector limits: maxThreads={}, minSpareThreads={}, acceptCount={}, maxConnections={}", + config.getHttpConnectorMaxThreads(), config.getHttpConnectorMinSpareThreads(), + config.getHttpConnectorAcceptCount(), config.getHttpConnectorMaxConnections()); + + if (config.isSslEnabled()) { + connector.setSecure(true); + connector.setScheme("https"); + connector.setProperty("SSLEnabled", "true"); + connector.setProperty("protocol", "org.apache.coyote.http11.Http11NioProtocol"); + connector.setProperty("keystoreFile", config.getKeystoreFile()); + connector.setProperty("keystorePass", config.getKeystorePassword()); + connector.setProperty("keystoreType", config.getKeystoreType()); + connector.setProperty("sslProtocol", "TLS"); + + if (config.isTruststoreEnabled()) { + connector.setProperty("truststoreFile", config.getTruststoreFile()); + connector.setProperty("truststorePass", config.getTruststorePassword()); + connector.setProperty("truststoreType", config.getTruststoreType()); + connector.setProperty("clientAuth", "true"); + } + } + + if (config.isHttp2Enabled()) { + connector.addUpgradeProtocol(new Http2Protocol()); + + LOG.info("HTTP/2 upgrade protocol registered on connector (port={})", config.getPort()); + } + + return connector; + } + + /** + * Registers {@link RangerPdpAuthNFilter} on all {@code /authz/*} paths. + * Init parameters are forwarded from the server config so the filter can + * instantiate and configure the auth handlers. + */ + private void addAuthFilter(Context ctx) { + FilterDef reqCtxFilterDef = new FilterDef(); + FilterMap reqCtxFilterMap = new FilterMap(); + FilterDef authFilterDef = new FilterDef(); + FilterMap authFilterMap = new FilterMap(); + + reqCtxFilterDef.setFilterName("rangerPdpRequestContextFilter"); + reqCtxFilterDef.setFilter(new RangerPdpRequestContextFilter()); + + reqCtxFilterMap.setFilterName("rangerPdpRequestContextFilter"); + reqCtxFilterMap.addURLPattern("/*"); + + authFilterDef.setFilterName("rangerPdpAuthFilter"); + authFilterDef.setFilter(new RangerPdpAuthNFilter()); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_TYPES, config.getAuthnTypes()); + + // HTTP Header auth + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, Boolean.toString(config.isHeaderAuthnEnabled())); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, config.getHeaderAuthnUsername()); + + // JWT bearer token auth + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, Boolean.toString(config.isJwtAuthnEnabled())); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, config.getJwtProviderUrl()); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, config.getJwtPublicKey()); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, config.getJwtCookieName()); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, config.getJwtAudiences()); + + // Kerberos / SPNEGO + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED, Boolean.toString(config.isKerberosAuthnEnabled())); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL, config.getSpnegoPrincipal()); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB, config.getSpnegoKeytab()); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, config.getKerberosNameRules()); + authFilterDef.addInitParameter(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, String.valueOf(config.getKerberosTokenValiditySeconds())); + + authFilterMap.setFilterName("rangerPdpAuthFilter"); + authFilterMap.addURLPattern("/authz/*"); + + ctx.addFilterDef(reqCtxFilterDef); + ctx.addFilterMap(reqCtxFilterMap); + ctx.addFilterDef(authFilterDef); + ctx.addFilterMap(authFilterMap); + } + + /** + * Registers the Jersey {@link ServletContainer} backed by {@link RangerPdpApplication}. + * The Jersey application binds the {@link RangerAuthorizer} singleton via HK2 so + * that it can be injected into {@link org.apache.ranger.pdp.rest.RangerPdpREST}. + */ + private void addJerseyServlet(Context ctx) { + RangerPdpApplication jerseyApp = new RangerPdpApplication(authorizer, config); + Servlet jerseyServlet = new ServletContainer(jerseyApp); + + Tomcat.addServlet(ctx, "jerseyServlet", jerseyServlet).setLoadOnStartup(1); + + ctx.addServletMappingDecoded("/authz/*", "jerseyServlet"); + } + + private void addStatusEndpoints(Context ctx) { + HttpServlet liveServlet = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.LIVE); + HttpServlet readyServlet = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.READY); + HttpServlet metricsServlet = new RangerPdpStatusServlet(runtimeStats, RangerPdpStatusServlet.Mode.METRICS); + + Tomcat.addServlet(ctx, "pdpLiveServlet", liveServlet).setLoadOnStartup(1); + Tomcat.addServlet(ctx, "pdpReadyServlet", readyServlet).setLoadOnStartup(1); + Tomcat.addServlet(ctx, "pdpMetricsServlet", metricsServlet).setLoadOnStartup(1); + + ctx.addServletMappingDecoded("/health/live", "pdpLiveServlet"); + ctx.addServletMappingDecoded("/health/ready", "pdpReadyServlet"); + ctx.addServletMappingDecoded("/metrics", "pdpMetricsServlet"); + } + + private void addAccessLogValve() { + String logDir = config.getLogDir(); + + new File(logDir).mkdirs(); + + AccessLogValve valve = new AccessLogValve(); + + valve.setDirectory(logDir); + valve.setPrefix("ranger-pdp-access"); + valve.setSuffix(".log"); + valve.setPattern("%h %l %u %t \"%r\" %s %b %D"); + valve.setRotatable(true); + + tomcat.getHost().getPipeline().addValve(valve); + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java new file mode 100644 index 0000000000..c11c42d8ef --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStats.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +public class RangerPdpStats { + private final AtomicBoolean serverStarted = new AtomicBoolean(false); + private final AtomicBoolean authorizerInitialized = new AtomicBoolean(false); + private final AtomicBoolean acceptingRequests = new AtomicBoolean(false); + + private final AtomicLong totalRequests = new AtomicLong(0); + private final AtomicLong totalAuthzSuccess = new AtomicLong(0); + private final AtomicLong totalAuthzBadRequest = new AtomicLong(0); + private final AtomicLong totalAuthzErrors = new AtomicLong(0); + private final AtomicLong totalAuthFailures = new AtomicLong(0); + private final AtomicLong totalLatencyNanos = new AtomicLong(0); + + public boolean isServerStarted() { + return serverStarted.get(); + } + + public void setServerStarted(boolean value) { + serverStarted.set(value); + } + + public boolean isAuthorizerInitialized() { + return authorizerInitialized.get(); + } + + public void setAuthorizerInitialized(boolean value) { + authorizerInitialized.set(value); + } + + public boolean isAcceptingRequests() { + return acceptingRequests.get(); + } + + public void setAcceptingRequests(boolean value) { + acceptingRequests.set(value); + } + + public void recordRequestSuccess(long elapsedNanos) { + totalRequests.incrementAndGet(); + totalAuthzSuccess.incrementAndGet(); + totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos)); + } + + public void recordRequestBadRequest(long elapsedNanos) { + totalRequests.incrementAndGet(); + totalAuthzBadRequest.incrementAndGet(); + totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos)); + } + + public void recordRequestError(long elapsedNanos) { + totalRequests.incrementAndGet(); + totalAuthzErrors.incrementAndGet(); + totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos)); + } + + public void recordAuthFailure(long elapsedNanos) { + totalRequests.incrementAndGet(); + totalAuthFailures.incrementAndGet(); + totalLatencyNanos.addAndGet(Math.max(0L, elapsedNanos)); + } + + public long getTotalRequests() { + return totalRequests.get(); + } + + public long getTotalAuthzSuccess() { + return totalAuthzSuccess.get(); + } + + public long getTotalAuthzBadRequest() { + return totalAuthzBadRequest.get(); + } + + public long getTotalAuthzErrors() { + return totalAuthzErrors.get(); + } + + public long getTotalAuthFailures() { + return totalAuthFailures.get(); + } + + public long getTotalLatencyNanos() { + return totalLatencyNanos.get(); + } + + public long getAverageLatencyMs() { + long requests = totalRequests.get(); + return requests > 0 ? (totalLatencyNanos.get() / requests) / 1_000_000L : 0L; + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java new file mode 100644 index 0000000000..9725909c89 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/RangerPdpStatusServlet.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer; +import org.apache.ranger.pdp.config.RangerPdpConstants; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +public class RangerPdpStatusServlet extends HttpServlet { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final RangerPdpStats runtimeState; + private final Mode mode; + + public enum Mode { LIVE, READY, METRICS } + + public RangerPdpStatusServlet(RangerPdpStats runtimeState, Mode mode) { + this.runtimeState = runtimeState; + this.mode = mode; + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + switch (mode) { + case LIVE: + writeLive(resp); + break; + case READY: + writeReady(req, resp); + break; + case METRICS: + writeMetrics(req, resp); + break; + default: + resp.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } + + private void writeLive(HttpServletResponse resp) throws IOException { + Map payload = new LinkedHashMap<>(); + + payload.put("status", runtimeState.isServerStarted() ? "UP" : "DOWN"); + payload.put("service", "ranger-pdp"); + payload.put("live", runtimeState.isServerStarted()); + + resp.setStatus(runtimeState.isServerStarted() ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE); + resp.setContentType(MediaType.APPLICATION_JSON); + + MAPPER.writeValue(resp.getOutputStream(), payload); + } + + private void writeReady(HttpServletRequest req, HttpServletResponse resp) throws IOException { + boolean ready = runtimeState.isServerStarted() && runtimeState.isAuthorizerInitialized() && runtimeState.isAcceptingRequests(); + + Map payload = new LinkedHashMap<>(); + + payload.put("status", ready ? "READY" : "NOT_READY"); + payload.put("service", "ranger-pdp"); + payload.put("ready", ready); + payload.put("authorizerInitialized", runtimeState.isAuthorizerInitialized()); + payload.put("acceptingRequests", runtimeState.isAcceptingRequests()); + payload.put("loadedServicesCount", getLoadedServicesCount(req)); + + resp.setStatus(ready ? HttpServletResponse.SC_OK : HttpServletResponse.SC_SERVICE_UNAVAILABLE); + resp.setContentType(MediaType.APPLICATION_JSON); + + MAPPER.writeValue(resp.getOutputStream(), payload); + } + + private void writeMetrics(HttpServletRequest req, HttpServletResponse resp) throws IOException { + StringBuilder sb = new StringBuilder(512); + + sb.append("# TYPE ranger_pdp_requests_total counter\n"); + sb.append("ranger_pdp_requests_total ").append(runtimeState.getTotalRequests()).append('\n'); + sb.append("# TYPE ranger_pdp_requests_success_total counter\n"); + sb.append("ranger_pdp_requests_success_total ").append(runtimeState.getTotalAuthzSuccess()).append('\n'); + sb.append("# TYPE ranger_pdp_requests_bad_request_total counter\n"); + sb.append("ranger_pdp_requests_bad_request_total ").append(runtimeState.getTotalAuthzBadRequest()).append('\n'); + sb.append("# TYPE ranger_pdp_requests_error_total counter\n"); + sb.append("ranger_pdp_requests_error_total ").append(runtimeState.getTotalAuthzErrors()).append('\n'); + sb.append("# TYPE ranger_pdp_auth_failures_total counter\n"); + sb.append("ranger_pdp_auth_failures_total ").append(runtimeState.getTotalAuthFailures()).append('\n'); + sb.append("# TYPE ranger_pdp_request_latency_avg_ms gauge\n"); + sb.append("ranger_pdp_request_latency_avg_ms ").append(runtimeState.getAverageLatencyMs()).append('\n'); + sb.append("# TYPE ranger_pdp_loaded_services_count gauge\n"); + sb.append("ranger_pdp_loaded_services_count ").append(getLoadedServicesCount(req)).append('\n'); + + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/plain; version=0.0.4"); + + resp.getWriter().write(sb.toString()); + } + + private int getLoadedServicesCount(HttpServletRequest req) { + ServletContext context = req.getServletContext(); + Object authorizer = context != null ? context.getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_AUTHORIZER) : null; + Set services = authorizer instanceof RangerEmbeddedAuthorizer ? ((RangerEmbeddedAuthorizer) authorizer).getLoadedServices() : null; + + return services != null ? services.size() : 0; + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java new file mode 100644 index 0000000000..11aea39ea8 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java @@ -0,0 +1,281 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.config; + +import org.apache.commons.lang3.StringUtils; +import org.apache.ranger.plugin.util.XMLUtils; +import org.ietf.jgss.GSSCredential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Properties; + +/** + * Reads Ranger PDP configuration from {@code ranger-pdp-default.xml} (classpath) + * overridden by {@code ranger-pdp-site.xml} (classpath or filesystem). + * + *

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.authn.kerberos.*} + *
  • JWT bearer token: {@code ranger.pdp.authn.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 = new Properties(); + + public RangerPdpConfig() { + loadFromClasspath(DEFAULT_CONFIG_FILE); + loadFromClasspath(SITE_CONFIG_FILE); + + String confDir = System.getProperty(RangerPdpConstants.PROP_CONF_DIR, ""); + + if (StringUtils.isNotBlank(confDir)) { + loadFromFile(new File(confDir, SITE_CONFIG_FILE)); + } + + applySystemPropertyOverrides(); + + LOG.info("RangerPdpConfig initialized (conf.dir={})", confDir); + } + + public int getPort() { + return getInt(RangerPdpConstants.PROP_PORT, 6500); + } + + public String getLogDir() { + return get(RangerPdpConstants.PROP_LOG_DIR, "/var/log/ranger/pdp"); + } + + public boolean isSslEnabled() { + return getBoolean(RangerPdpConstants.PROP_SSL_ENABLED, false); + } + + public String getKeystoreFile() { + return get(RangerPdpConstants.PROP_SSL_KEYSTORE_FILE, ""); + } + + public String getKeystorePassword() { + return get(RangerPdpConstants.PROP_SSL_KEYSTORE_PASSWORD, ""); + } + + public String getKeystoreType() { + return get(RangerPdpConstants.PROP_SSL_KEYSTORE_TYPE, "JKS"); + } + + public boolean isTruststoreEnabled() { + return getBoolean(RangerPdpConstants.PROP_SSL_TRUSTSTORE_ENABLED, false); + } + + public String getTruststoreFile() { + return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_FILE, ""); + } + + public String getTruststorePassword() { + return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_PASSWORD, ""); + } + + public String getTruststoreType() { + return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_TYPE, "JKS"); + } + + public boolean isHttp2Enabled() { + return getBoolean(RangerPdpConstants.PROP_HTTP2_ENABLED, true); + } + + public int getHttpConnectorMaxThreads() { + return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_THREADS, 200); + } + + public int getHttpConnectorMinSpareThreads() { + return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MIN_SPARE_THREADS, 20); + } + + public int getHttpConnectorAcceptCount() { + return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_ACCEPT_COUNT, 100); + } + + public int getHttpConnectorMaxConnections() { + return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_CONNECTIONS, 10000); + } + + public String getAuthnTypes() { + return get(RangerPdpConstants.PROP_AUTHN_TYPES, "header,jwt,kerberos"); + } + + // --- HTTP Header auth --- + public boolean isHeaderAuthnEnabled() { + return getBoolean(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, false); + } + + public String getHeaderAuthnUsername() { + return get(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Forwarded-User"); + } + + // --- JWT bearer token auth --- + public boolean isJwtAuthnEnabled() { + return getBoolean(RangerPdpConstants.PROP_AUTHN_JWT_ENABLED, false); + } + + public String getJwtProviderUrl() { + return get(RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, ""); + } + + public String getJwtPublicKey() { + return get(RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, ""); + } + + public String getJwtCookieName() { + return get(RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, "hadoop-jwt"); + } + + public String getJwtAudiences() { + return get(RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, ""); + } + + // --- Kerberos / SPNEGO --- + public boolean isKerberosAuthnEnabled() { + return getBoolean(RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED, false); + } + + public String getSpnegoPrincipal() { + return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL, ""); + } + + public String getSpnegoKeytab() { + return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB, ""); + } + + public int getKerberosTokenValiditySeconds() { + return getInt(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY, GSSCredential.INDEFINITE_LIFETIME); + } + + public String getKerberosNameRules() { + return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, "DEFAULT"); + } + + /** + * Returns all properties for forwarding to {@code RangerEmbeddedAuthorizer}. + */ + public Properties getAuthzProperties() { + return new Properties(props); + } + + public String get(String key, String defaultValue) { + String val = props.getProperty(key); + + return StringUtils.isNotBlank(val) ? val.trim() : defaultValue; + } + + public int getInt(String key, int defaultValue) { + String val = props.getProperty(key); + + if (StringUtils.isNotBlank(val)) { + try { + return Integer.parseInt(val.trim()); + } catch (NumberFormatException e) { + LOG.warn("Invalid integer for {}: '{}'; using default {}", key, val, defaultValue); + } + } + + return defaultValue; + } + + public boolean getBoolean(String key, boolean defaultValue) { + String val = props.getProperty(key); + + return StringUtils.isNotBlank(val) ? Boolean.parseBoolean(val.trim()) : defaultValue; + } + + private void loadFromClasspath(String resourceName) { + try (InputStream in = getClass().getClassLoader().getResourceAsStream(resourceName)) { + if (in != null) { + parseHadoopXml(in, resourceName); + } else { + LOG.debug("Config resource not found on classpath: {}", resourceName); + } + } catch (IOException e) { + LOG.warn("Failed to close stream for classpath resource: {}", resourceName, e); + } + } + + private void loadFromFile(File file) { + if (!file.exists() || !file.isFile()) { + LOG.debug("Config file not found: {}", file); + return; + } + + try (InputStream in = Files.newInputStream(file.toPath())) { + parseHadoopXml(in, file.getAbsolutePath()); + } catch (IOException e) { + LOG.warn("Failed to read config file: {}", file, e); + } + } + + /** + * Parses a Hadoop-style {@code } 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) { + LOG.info("Loading from {}. Properties count {}", source, props.size()); + + XMLUtils.loadConfig(in, props); + + LOG.info("Loaded from {}. Properties count {}", source, props.size()); + } + + /** + * Apply JVM system-property overrides for operationally sensitive keys so Kubernetes + * (or any orchestrator) can drive runtime config with JAVA_OPTS/-D flags. + */ + private void applySystemPropertyOverrides() { + for (String key : System.getProperties().stringPropertyNames()) { + if (key.startsWith(RangerPdpConstants.PROP_PDP_PREFIX) || key.startsWith(RangerPdpConstants.PROP_AUTHZ_PREFIX)) { + props.setProperty(key, System.getProperty(key)); + } + } + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java new file mode 100644 index 0000000000..c1a4313514 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConstants.java @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.config; + +public final class RangerPdpConstants { + private RangerPdpConstants() { + // no instances + } + + // Servlet context attributes + public static final String SERVLET_CTX_ATTR_CONFIG = "ranger.pdp.config"; + public static final String SERVLET_CTX_ATTR_AUTHORIZER = "ranger.pdp.authorizer"; + public static final String SERVLET_CTX_ATTR_RUNTIME_STATE = "ranger.pdp.runtime.state"; + + // Request attributes set by auth filter + public static final String ATTR_AUTHENTICATED_USER = "ranger.pdp.authenticated.user"; + public static final String ATTR_AUTHN_TYPE = "ranger.pdp.authn.type"; + + // Server + public static final String PROP_CONF_DIR = "ranger.pdp.conf.dir"; + public static final String PROP_PORT = "ranger.pdp.port"; + public static final String PROP_LOG_DIR = "ranger.pdp.log.dir"; + + // SSL/TLS + public static final String PROP_SSL_ENABLED = "ranger.pdp.ssl.enabled"; + public static final String PROP_SSL_KEYSTORE_FILE = "ranger.pdp.ssl.keystore.file"; + public static final String PROP_SSL_KEYSTORE_PASSWORD = "ranger.pdp.ssl.keystore.password"; + public static final String PROP_SSL_KEYSTORE_TYPE = "ranger.pdp.ssl.keystore.type"; + public static final String PROP_SSL_TRUSTSTORE_ENABLED = "ranger.pdp.ssl.truststore.enabled"; + public static final String PROP_SSL_TRUSTSTORE_FILE = "ranger.pdp.ssl.truststore.file"; + public static final String PROP_SSL_TRUSTSTORE_PASSWORD = "ranger.pdp.ssl.truststore.password"; + public static final String PROP_SSL_TRUSTSTORE_TYPE = "ranger.pdp.ssl.truststore.type"; + + // HTTP/2 + public static final String PROP_HTTP2_ENABLED = "ranger.pdp.http2.enabled"; + + // HTTP connector limits + public static final String PROP_HTTP_CONNECTOR_MAX_THREADS = "ranger.pdp.http.connector.maxThreads"; + public static final String PROP_HTTP_CONNECTOR_MIN_SPARE_THREADS = "ranger.pdp.http.connector.minSpareThreads"; + public static final String PROP_HTTP_CONNECTOR_ACCEPT_COUNT = "ranger.pdp.http.connector.acceptCount"; + public static final String PROP_HTTP_CONNECTOR_MAX_CONNECTIONS = "ranger.pdp.http.connector.maxConnections"; + + // Authentication types + public static final String PROP_AUTHN_PREFIX = "ranger.pdp.authn."; + public static final String PROP_AUTHN_TYPES = PROP_AUTHN_PREFIX + "types"; + + // HTTP header auth + public static final String PROP_AUTHN_HEADER_PREFIX = PROP_AUTHN_PREFIX + "header."; + public static final String PROP_AUTHN_HEADER_ENABLED = PROP_AUTHN_HEADER_PREFIX + "enabled"; + public static final String PROP_AUTHN_HEADER_USERNAME = PROP_AUTHN_HEADER_PREFIX + "username"; + + // JWT auth + public static final String PROP_AUTHN_JWT_PREFIX = PROP_AUTHN_PREFIX + "jwt."; + public static final String PROP_AUTHN_JWT_ENABLED = PROP_AUTHN_JWT_PREFIX + "enabled"; + public static final String PROP_AUTHN_JWT_PROVIDER_URL = PROP_AUTHN_JWT_PREFIX + "provider.url"; + public static final String PROP_AUTHN_JWT_PUBLIC_KEY = PROP_AUTHN_JWT_PREFIX + "public.key"; + public static final String PROP_AUTHN_JWT_COOKIE_NAME = PROP_AUTHN_JWT_PREFIX + "cookie.name"; + public static final String PROP_AUTHN_JWT_AUDIENCES = PROP_AUTHN_JWT_PREFIX + "audiences"; + + // Kerberos/SPNEGO auth + public static final String PROP_AUTHN_KERBEROS_PREFIX = PROP_AUTHN_PREFIX + "kerberos."; + public static final String PROP_AUTHN_KERBEROS_ENABLED = PROP_AUTHN_KERBEROS_PREFIX + "enabled"; + public static final String PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL = PROP_AUTHN_KERBEROS_PREFIX + "spnego.principal"; + public static final String PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB = PROP_AUTHN_KERBEROS_PREFIX + "spnego.keytab"; + public static final String PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY = PROP_AUTHN_KERBEROS_PREFIX + "token.valid.seconds"; + public static final String PROP_AUTHN_KERBEROS_NAME_RULES = PROP_AUTHN_KERBEROS_PREFIX + "name.rules"; + + // Authorizer/audit properties referenced by PDP code + public static final String PROP_AUTHZ_POLICY_CACHE_DIR = "ranger.authz.default.policy.cache.dir"; + public static final String PROP_AUTHZ_PREFIX = "ranger.authz."; + public static final String PROP_PDP_PREFIX = "ranger.pdp."; + public static final String PROP_PDP_SERVICE_PREFIX = PROP_PDP_PREFIX + "service."; + + // delegation users + public static final String PROP_SUFFIX_DELEGATION_USERS = ".delegation.users"; + public static final String WILDCARD_SERVICE_NAME = "*"; +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java b/pdp/src/main/java/org/apache/ranger/pdp/model/ErrorResponse.java new file mode 100644 index 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/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..1f447f706e --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java @@ -0,0 +1,491 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.rest; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.model.RangerAccessInfo; +import org.apache.ranger.authz.model.RangerAuthzRequest; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerResourceInfo; +import org.apache.ranger.authz.model.RangerResourcePermissions; +import org.apache.ranger.authz.model.RangerResourcePermissionsRequest; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.apache.ranger.pdp.RangerPdpStats; +import org.apache.ranger.pdp.config.RangerPdpConfig; +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.apache.ranger.pdp.model.ErrorResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.stream.Collectors; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.FORBIDDEN; +import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; +import static javax.ws.rs.core.Response.Status.OK; +import static javax.ws.rs.core.Response.Status.UNAUTHORIZED; +import static org.apache.ranger.authz.api.RangerAuthzApiErrorCode.INVALID_REQUEST_USER_INFO_MISSING; +import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_PDP_SERVICE_PREFIX; +import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_SUFFIX_DELEGATION_USERS; +import static org.apache.ranger.pdp.config.RangerPdpConstants.WILDCARD_SERVICE_NAME; + +/** + * REST resource that exposes the three core {@link RangerAuthorizer} methods over HTTP. + * + *

All endpoints are under {@code /authz/v1} and produce/consume {@code application/json}. + * Authentication is enforced upstream by {@link org.apache.ranger.pdp.security.RangerPdpAuthNFilter}; the authenticated + * caller's identity is read from the {@link RangerPdpConstants#ATTR_AUTHENTICATED_USER} + * request attribute. + * + * + * + * + * + * + * + * + * + * + *
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 (isStatusOk(ret)) { + try { + RangerAuthzResult result = authorizer.authorize(request); + + ret = Response.ok(result).build(); + } catch (RangerAuthzException e) { + LOG.warn("authorize(requestId={}): authorization error; caller={}", requestId, caller, e); + + ret = badRequest(e); + } catch (Exception e) { + LOG.error("authorize(requestId={}): internal error; caller={}", requestId, caller, e); + + ret = serverError(); + } + } + + LOG.debug("<== authorize(requestId={}, caller={}, serviceName={}): ret={}", requestId, caller, serviceName, ret != null ? ret.getStatus() : null); + } finally { + recordRequestMetrics(ret, startNanos, httpRequest); + } + + return ret; + } + + /** + * Evaluates multiple access requests in a single call. + * + * @param request the multi-access authorization request + * @return {@code 200 OK} with {@link RangerMultiAuthzResult}, or {@code 400} / {@code 500} on error + */ + @POST + @Path("/authorizeMulti") + public Response authorizeMulti(RangerMultiAuthzRequest request, @Context HttpServletRequest httpRequest) { + long startNanos = System.nanoTime(); + Response ret = null; + + try { + String requestId = request != null ? request.getRequestId() : null; + String caller = getAuthenticatedUser(httpRequest); + String serviceName = getServiceName(request); + RangerUserInfo user = request != null ? request.getUser() : null; + List accesses = request != null ? request.getAccesses() : null; + + LOG.debug("==> authorizeMulti(requestId={}, caller={}, serviceName={})", requestId, caller, serviceName); + + ret = validateCaller(caller, user, accesses, serviceName); + + if (isStatusOk(ret)) { + try { + RangerMultiAuthzResult result = authorizer.authorize(request); + + ret = Response.ok(result).build(); + } catch (RangerAuthzException e) { + LOG.warn("authorizeMulti(requestId={}): authorization error; caller={}", requestId, caller, e); + + ret = badRequest(e); + } catch (Exception e) { + LOG.error("authorizeMulti(requestId={}): internal error; caller={}", requestId, caller, e); + + ret = serverError(); + } + } + + LOG.debug("<== authorizeMulti(requestId={}, caller={}, serviceName={}): ret={}", requestId, caller, serviceName, ret != null ? ret.getStatus() : null); + } finally { + recordRequestMetrics(ret, startNanos, httpRequest); + } + + return ret; + } + + /** + * Returns the effective permissions for a resource, broken down by user/group/role. + * + * @param request wrapper containing the resource info and access context + * @return {@code 200 OK} with {@link RangerResourcePermissions}, or {@code 400} / {@code 500} on error + */ + @POST + @Path("/permissions") + public Response getResourcePermissions(RangerResourcePermissionsRequest request, @Context HttpServletRequest httpRequest) { + long startNanos = System.nanoTime(); + Response ret = null; + + try { + String caller = getAuthenticatedUser(httpRequest); + String serviceName = getServiceName(request); + + LOG.debug("==> getResourcePermissions(caller={}, serviceName={})", caller, serviceName); + + ret = validateCaller(caller, serviceName); + + if (isStatusOk(ret)) { + try { + RangerResourcePermissions result = authorizer.getResourcePermissions(request); + + ret = Response.ok(result).build(); + } catch (RangerAuthzException e) { + LOG.warn("getResourcePermissions(): validation error; caller={}", caller, e); + + ret = badRequest(e); + } catch (Exception e) { + LOG.error("getResourcePermissions(): unexpected error; caller={}", caller, e); + + ret = serverError(); + } + } + + LOG.debug("<== getResourcePermissions(caller={}, serviceName={}): ret={}", caller, serviceName, ret != null ? ret.getStatus() : null); + } finally { + recordRequestMetrics(ret, startNanos, httpRequest); + } + + return ret; + } + + private String getAuthenticatedUser(HttpServletRequest httpRequest) { + Object user = httpRequest.getAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER); + + return user != null ? user.toString() : null; + } + + private static String getServiceName(RangerAuthzRequest request) { + return request != null && request.getContext() != null ? request.getContext().getServiceName() : null; + } + + private static String getServiceName(RangerMultiAuthzRequest request) { + return request != null && request.getContext() != null ? request.getContext().getServiceName() : null; + } + + private static String getServiceName(RangerResourcePermissionsRequest request) { + return request != null && request.getContext() != null ? request.getContext().getServiceName() : null; + } + + private Response validateCaller(String caller, RangerUserInfo user, RangerAccessInfo access, String serviceName) { + final Response ret; + + if (StringUtils.isBlank(caller)) { + ret = Response.status(UNAUTHORIZED) + .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required")) + .build(); + } else if (user == null || StringUtils.isBlank(user.getName())) { + ret = Response.status(BAD_REQUEST) + .entity(new ErrorResponse(BAD_REQUEST, INVALID_REQUEST_USER_INFO_MISSING.getMessage())) + .build(); + } else { + boolean needsDelegation = isDelegationNeeded(caller, user) || isDelegationNeeded(access); + + if (needsDelegation) { + if (!isDelegationUserForService(serviceName, caller)) { + LOG.info("{} is not a delegation user in service {}", caller, serviceName); + + ret = Response.status(FORBIDDEN) + .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized")) + .build(); + } else { + ret = RESPONSE_OK; + } + } else { + ret = RESPONSE_OK; + } + } + + return ret; + } + + private Response validateCaller(String caller, RangerUserInfo user, List accesses, String serviceName) { + final Response ret; + + if (StringUtils.isBlank(caller)) { + ret = Response.status(UNAUTHORIZED) + .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required")) + .build(); + } else if (user == null || StringUtils.isBlank(user.getName())) { + ret = Response.status(BAD_REQUEST) + .entity(new ErrorResponse(BAD_REQUEST, INVALID_REQUEST_USER_INFO_MISSING.getMessage())) + .build(); + } else { + boolean needsDelegation = isDelegationNeeded(caller, user) || isDelegationNeeded(accesses); + + if (needsDelegation) { + if (!isDelegationUserForService(serviceName, caller)) { + LOG.info("{} is not a delegation user in service {}", caller, serviceName); + + ret = Response.status(FORBIDDEN) + .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized")) + .build(); + } else { + ret = RESPONSE_OK; + } + } else { + ret = RESPONSE_OK; + } + } + + return ret; + } + + private Response validateCaller(String caller, String serviceName) { + final Response ret; + + if (StringUtils.isBlank(caller)) { + ret = Response.status(UNAUTHORIZED) + .entity(new ErrorResponse(UNAUTHORIZED, "Authentication required")) + .build(); + } else if (!isDelegationUserForService(serviceName, caller)) { + LOG.info("{} is not a delegation user in service {}", caller, serviceName); + + ret = Response.status(FORBIDDEN) + .entity(new ErrorResponse(FORBIDDEN, caller + " is not authorized")) + .build(); + } else { + ret = RESPONSE_OK; + } + + return ret; + } + + private boolean isDelegationNeeded(String caller, RangerUserInfo user) { + String userName = user.getName(); + boolean needsDelegation = !caller.equals(userName); + + if (!needsDelegation) { + // don't trust user-attributes/groups/roles if caller doesn't have delegation permission + needsDelegation = MapUtils.isNotEmpty(user.getAttributes()) || CollectionUtils.isNotEmpty(user.getGroups()) || CollectionUtils.isNotEmpty(user.getRoles()); + } + + return needsDelegation; + } + + private boolean isDelegationNeeded(RangerAccessInfo access) { + RangerResourceInfo resource = access != null ? access.getResource() : null; + + // delegation permission is needed when resource attributes are specified + return (resource != null && MapUtils.isNotEmpty(resource.getAttributes())); + } + + private boolean isDelegationNeeded(List accesses) { + if (accesses != null) { + for (RangerAccessInfo access : accesses) { + if (isDelegationNeeded(access)) { + return true; + } + } + } + + return false; + } + + private RangerPdpStats getRuntimeState(HttpServletRequest httpRequest) { + Object state = httpRequest.getServletContext().getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE); + + if (state instanceof RangerPdpStats) { + return (RangerPdpStats) state; + } else { + Object fallback = servletContext != null ? servletContext.getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE) : null; + + return (fallback instanceof RangerPdpStats) ? (RangerPdpStats) fallback : new RangerPdpStats(); + } + } + + private void recordRequestMetrics(Response ret, long startNanos, HttpServletRequest httpRequest) { + RangerPdpStats state = getRuntimeState(httpRequest); + int status = ret != null ? ret.getStatus() : 500; + long elapsed = System.nanoTime() - startNanos; + + if (status >= 200 && status < 300) { + state.recordRequestSuccess(elapsed); + } else if (status == 401 || status == 403) { // UNAUTHORIZED or FORBIDDEN + state.recordAuthFailure(elapsed); + } else if (status == 400) { + state.recordRequestBadRequest(elapsed); + } else { + state.recordRequestError(elapsed); + } + } + + private boolean isDelegationUserForService(String serviceName, String userName) { + final boolean ret; + Map> 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(BAD_REQUEST) + .entity(new ErrorResponse(BAD_REQUEST, e.getMessage())) + .build(); + } + + private Response serverError() { + return Response.serverError() + .entity(new ErrorResponse(INTERNAL_SERVER_ERROR, "Internal Server Error")) + .build(); + } + + private boolean isStatusOk(Response resp) { + return resp != null && resp.getStatus() == OK.getStatusCode(); + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java new file mode 100644 index 0000000000..60df86dafa --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +import org.apache.commons.lang3.StringUtils; +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.Properties; + +/** + * Authenticates requests by extracting the username from a trusted HTTP request header. + * + *

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 HttpHeaderAuthNHandler implements PdpAuthNHandler { + private static final Logger LOG = LoggerFactory.getLogger(HttpHeaderAuthNHandler.class); + + public static final String AUTH_TYPE = "HEADER"; + + private String usernameHeader; + + @Override + public void init(Properties config) { + usernameHeader = config.getProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Forwarded-User"); + + LOG.info("HttpHeaderAuthHandler initialized; username header={}", usernameHeader); + } + + @Override + public Result authenticate(HttpServletRequest request, HttpServletResponse response) { + String userName = request.getHeader(usernameHeader); + + LOG.debug("authenticate(): user={} (from header {})", userName, usernameHeader); + + return StringUtils.isBlank(userName) ? Result.skip() : Result.authenticated(userName.trim(), AUTH_TYPE); + } + + @Override + public String getChallengeHeader() { + return null; + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java new file mode 100644 index 0000000000..4e45c00311 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthNHandler.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +import org.apache.ranger.authz.handler.RangerAuth; +import org.apache.ranger.authz.handler.jwt.RangerDefaultJwtAuthHandler; +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.Properties; + +/** + * Authenticates requests using a JWT bearer token. + * + *

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.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) + *
  • {@code cookie.name} – JWT cookie name (default: {@code hadoop-jwt}) + *
  • {@code audiences} – comma-separated list of accepted audiences (optional) + *
+ */ +public class JwtAuthNHandler implements PdpAuthNHandler { + private static final Logger LOG = LoggerFactory.getLogger(JwtAuthNHandler.class); + + public static final String AUTH_TYPE = "JWT"; + + private RangerDefaultJwtAuthHandler delegate; + + @Override + public void init(Properties config) throws Exception { + Properties jwtConfig = new Properties(); + + copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL, jwtConfig, RangerDefaultJwtAuthHandler.KEY_PROVIDER_URL); + copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_PUBLIC_KEY); + copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_COOKIE_NAME); + copyIfPresent(config, RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES, jwtConfig, RangerDefaultJwtAuthHandler.KEY_JWT_AUDIENCES); + + delegate = new RangerDefaultJwtAuthHandler(); + + delegate.initialize(jwtConfig); + + LOG.info("JwtAuthHandler initialized"); + } + + @Override + public Result authenticate(HttpServletRequest request, HttpServletResponse response) { + if (!RangerDefaultJwtAuthHandler.canAuthenticateRequest(request)) { + return Result.skip(); + } + + RangerAuth rangerAuth = delegate.authenticate(request); + + if (rangerAuth != null && rangerAuth.isAuthenticated()) { + LOG.debug("authenticate(): user={}", rangerAuth.getUserName()); + + return Result.authenticated(rangerAuth.getUserName(), AUTH_TYPE); + } + + LOG.warn("authenticate(): JWT validation failed"); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.addHeader("WWW-Authenticate", getChallengeHeader() + ", error=\"invalid_token\""); + + return Result.challenge(); + } + + @Override + public String getChallengeHeader() { + return "Bearer realm=\"Ranger PDP\""; + } + + private void copyIfPresent(Properties src, String srcKey, Properties dst, String dstKey) { + String val = src.getProperty(srcKey); + + if (val != null && !val.isEmpty()) { + dst.setProperty(dstKey, val); + } + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java new file mode 100644 index 0000000000..e46e3e5d38 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthNHandler.java @@ -0,0 +1,275 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.security.authentication.util.KerberosName; +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * Authenticates requests using Kerberos SPNEGO (HTTP Negotiate). + * + *

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

    + *
  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 KerberosAuthNHandler implements PdpAuthNHandler { + private static final Logger LOG = LoggerFactory.getLogger(KerberosAuthNHandler.class); + + public static final String AUTH_TYPE = "KERBEROS"; + + private static final String NEGOTIATE_PREFIX = "Negotiate "; + private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + private static final String AUTHORIZATION = "Authorization"; + private static final Oid SPNEGO_OID; + private static final Oid KRB5_OID; + + static { + try { + SPNEGO_OID = new Oid("1.3.6.1.5.5.2"); + KRB5_OID = new Oid("1.2.840.113554.1.2.2"); + } catch (GSSException e) { + throw new ExceptionInInitializerError(e); + } + } + + private Subject serviceSubject; + private GSSManager gssManager; + private GSSCredential serverCred; + + @Override + public void init(Properties config) throws Exception { + String principal = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL); + String keytab = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB); + String nameRules = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, "DEFAULT"); + String tokenValidity = config.getProperty(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY); + + int tokenLifetime = StringUtils.isBlank(tokenValidity) ? GSSCredential.INDEFINITE_LIFETIME : Integer.parseInt(tokenValidity); + + if (StringUtils.isBlank(principal) || StringUtils.isBlank(keytab)) { + throw new IllegalArgumentException("Kerberos auth requires configurations " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL + " and " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB); + } + + serviceSubject = loginWithKeytab(principal, keytab); + + initializeKerberosNameRules(nameRules); + + gssManager = GSSManager.getInstance(); + + GSSName serverName = gssManager.createName(principal, GSSName.NT_USER_NAME); + + // Create acceptor credentials in the logged-in Subject to avoid fallback to JVM default keytab. + serverCred = Subject.doAs(serviceSubject, (PrivilegedExceptionAction) () -> + gssManager.createCredential(serverName, tokenLifetime, new Oid[] {SPNEGO_OID, KRB5_OID}, GSSCredential.ACCEPT_ONLY)); + + LOG.info("KerberosAuthNHandler initialized; principal={} (bound acceptor credential to configured principal)", principal); + } + + @Override + public Result authenticate(HttpServletRequest request, HttpServletResponse response) { + String authHeader = request.getHeader(AUTHORIZATION); + + if (authHeader == null || !authHeader.startsWith(NEGOTIATE_PREFIX)) { + return Result.skip(); + } + + byte[] inputToken = Base64.decodeBase64(authHeader.substring(NEGOTIATE_PREFIX.length()).trim()); + + try { + return Subject.doAs(serviceSubject, (PrivilegedExceptionAction) () -> validateSpnegoToken(inputToken, response)); + } catch (Exception e) { + LOG.warn("authenticate(): SPNEGO validation error", e); + + sendUnauthorized(response, null); + + return Result.challenge(); + } + } + + @Override + public String getChallengeHeader() { + return NEGOTIATE_PREFIX.trim(); + } + + private Result validateSpnegoToken(byte[] inputToken, HttpServletResponse response) throws GSSException { + GSSContext gssCtx = gssManager.createContext(serverCred); + + try { + byte[] outputToken = gssCtx.acceptSecContext(inputToken, 0, inputToken.length); + + if (outputToken != null && outputToken.length > 0) { + response.addHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX + Base64.encodeBase64String(outputToken)); + } + + if (!gssCtx.isEstablished()) { + sendUnauthorized(response, outputToken); + + return Result.challenge(); + } + + String principal = gssCtx.getSrcName().toString(); + String userName = applyNameRules(principal); + + LOG.debug("authenticate(): SPNEGO success, principal={}, user={}", principal, userName); + + return Result.authenticated(userName, AUTH_TYPE); + } finally { + try { + gssCtx.dispose(); + } catch (GSSException excp) { + LOG.debug("GSSContext.dispose() failed. Ignored", excp); + } + } + } + + private void sendUnauthorized(HttpServletResponse response, byte[] outputToken) { + if (!response.isCommitted()) { + if (outputToken != null && outputToken.length > 0) { + response.setHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX + Base64.encodeBase64String(outputToken)); + } else { + response.setHeader(WWW_AUTHENTICATE, NEGOTIATE_PREFIX.trim()); + } + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + } + + /** + * Applies configured auth_to_local rules. + * + *

Uses Hadoop {@link KerberosName} with configured auth_to_local rules. + * Falls back to DEFAULT short-name mapping only if transformation fails. + */ + private String applyNameRules(String principal) { + try { + String shortName = new KerberosName(principal).getShortName(); + + if (StringUtils.isNotBlank(shortName)) { + return shortName; + } + } catch (Exception e) { + LOG.warn("Failed KerberosName transformation for principal '{}'; using DEFAULT short-name mapping", principal, e); + } + + return defaultShortName(principal); + } + + private void initializeKerberosNameRules(String configuredRules) { + String effectiveRules = StringUtils.defaultIfBlank(configuredRules, "DEFAULT").trim(); + + KerberosName.setRules(effectiveRules); + + LOG.info("Initialized Kerberos name rules: {}='{}'", RangerPdpConstants.PROP_AUTHN_KERBEROS_NAME_RULES, effectiveRules); + } + + private String defaultShortName(String principal) { + String name = principal; + int atSign = name.indexOf('@'); + + if (atSign >= 0) { + name = name.substring(0, atSign); + } + + int slash = name.indexOf('/'); + + if (slash >= 0) { + name = name.substring(0, slash); + } + + return name; + } + + private Subject loginWithKeytab(String principal, String keytab) throws LoginException { + Configuration jaasConfig = new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + Map opts = new HashMap<>(); + + opts.put("useKeyTab", "true"); + opts.put("storeKey", "true"); + opts.put("doNotPrompt", "true"); + opts.put("useTicketCache", "false"); + opts.put("keyTab", keytab); + opts.put("principal", principal); + opts.put("refreshKrb5Config", "true"); + opts.put("isInitiator", "false"); + + return new AppConfigurationEntry[] { + new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, opts) + }; + } + }; + + LoginContext lc = new LoginContext("Ranger-PDP-Kerberos", null, noOpCallbackHandler(), jaasConfig); + + lc.login(); + + return lc.getSubject(); + } + + private static CallbackHandler noOpCallbackHandler() { + return (Callback[] callbacks) -> { + for (Callback cb : callbacks) { + LOG.warn("Unexpected JAAS callback: {}", cb.getClass().getName()); + + throw new UnsupportedCallbackException(cb); + } + }; + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java new file mode 100644 index 0000000000..4bb11aea5b --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/PdpAuthNHandler.java @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Properties; + +/** + * Contract for PDP authentication handlers. + * + *

Each handler is responsible for a single credential type (Kerberos, JWT, HTTP Header). + * The {@link RangerPdpAuthNFilter} tries handlers in configured order and uses the first one + * that returns {@link Result.Status#AUTHENTICATED}. + */ +public interface PdpAuthNHandler { + /** + * Initializes the handler with filter init parameters. + * + * @param config filter init parameters + * @throws Exception on initialization failure + */ + void init(Properties config) throws Exception; + + /** + * Attempts to authenticate the incoming request. + * + *

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/RangerPdpAuthNFilter.java b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java new file mode 100644 index 0000000000..32d04ef0a4 --- /dev/null +++ b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilter.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +import org.apache.commons.lang3.StringUtils; +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_AUTHN_TYPES; + +/** + * Servlet filter that enforces authentication for all PDP REST endpoints. + * + *

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

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 RangerPdpAuthNFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(RangerPdpAuthNFilter.class); + + private final List handlers = new ArrayList<>(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + Properties config = toProperties(filterConfig); + String authnTypes = filterConfig.getInitParameter(PROP_AUTHN_TYPES); + + if (StringUtils.isNotBlank(authnTypes)) { + for (String authnType : authnTypes.split(",")) { + PdpAuthNHandler handler = createHandler(authnType.trim().toLowerCase(), filterConfig); + + if (handler == null) { + continue; + } + + try { + handler.init(config); + + handlers.add(handler); + + LOG.info("{}: successfully registered authentication handler", authnType); + } catch (Exception excp) { + LOG.error("{}: failed to initialize authentication handler. Handler disabled", authnType, excp); + } + } + } + + if (handlers.isEmpty()) { + throw new ServletException("No valid authentication handlers configured"); + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest httpReq = (HttpServletRequest) request; + HttpServletResponse httpResp = (HttpServletResponse) response; + + for (PdpAuthNHandler handler : handlers) { + PdpAuthNHandler.Result result = handler.authenticate(httpReq, httpResp); + + switch (result.getStatus()) { + case AUTHENTICATED: + httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER, result.getUserName()); + httpReq.setAttribute(RangerPdpConstants.ATTR_AUTHN_TYPE, result.getAuthType()); + + LOG.debug("doFilter(): authenticated user={}, type={}", result.getUserName(), result.getAuthType()); + + chain.doFilter(request, response); + return; + + case CHALLENGE: + // handler has already written the 401; stop processing + return; + + case SKIP: + default: + // try the next handler + break; + } + } + + // No handler could authenticate the request; send a 401 with all challenge headers + LOG.debug("doFilter(): no handler authenticated request from {}", httpReq.getRemoteAddr()); + + sendUnauthenticated(httpResp); + } + + @Override + public void destroy() { + handlers.clear(); + } + + private void sendUnauthenticated(HttpServletResponse response) throws IOException { + for (PdpAuthNHandler handler : handlers) { + String challenge = handler.getChallengeHeader(); + + if (StringUtils.isNotBlank(challenge)) { + response.addHeader("WWW-Authenticate", challenge); + } + } + + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + response.setContentType(MediaType.APPLICATION_JSON); + response.getWriter().write("{\"code\":\"UNAUTHORIZED\",\"message\":\"Authentication required\"}"); + } + + private PdpAuthNHandler createHandler(String type, FilterConfig filterConfig) { + final PdpAuthNHandler ret; + + switch (type) { + case "header": + ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED) ? new HttpHeaderAuthNHandler() : null; + break; + case "jwt": + ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_JWT_ENABLED) ? new JwtAuthNHandler() : null; + break; + case "kerberos": + ret = getBoolean(filterConfig, RangerPdpConstants.PROP_AUTHN_KERBEROS_ENABLED) ? new KerberosAuthNHandler() : null; + break; + default: + ret = null; + break; + } + + if (ret == null) { + LOG.warn("{}: authentication type unknown or disabled. Ignored", type); + } + + return ret; + } + + private Properties toProperties(FilterConfig filterConfig) { + Properties props = new Properties(); + java.util.Enumeration names = filterConfig.getInitParameterNames(); + + while (names.hasMoreElements()) { + String name = names.nextElement(); + + props.setProperty(name, filterConfig.getInitParameter(name)); + } + + return props; + } + + private boolean getBoolean(FilterConfig config, String name) { + return Boolean.parseBoolean(config.getInitParameter(name)); + } +} diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java b/pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpRequestContextFilter.java new file mode 100644 index 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..ddf2ff9346 --- /dev/null +++ b/pdp/src/main/resources/ranger-pdp-default.xml @@ -0,0 +1,364 @@ + + + + + + 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.authn.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.authn.jwt.enabled + false + Enable JWT authentication. + + + + ranger.pdp.authn.jwt.provider.url + + URL of the JWT provider (used to fetch the public key). + + + + ranger.pdp.authn.jwt.public.key + + PEM-encoded public key for verifying JWT signatures. + + + + ranger.pdp.authn.jwt.cookie.name + hadoop-jwt + Cookie name from which a JWT bearer token may be read. + + + + ranger.pdp.authn.jwt.audiences + + Comma-separated list of accepted JWT audiences. Empty means any audience is accepted. + + + + + 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. + + + + ranger.pdp.authn.kerberos.spnego.keytab + + Path to the keytab file for the SPNEGO service principal. + + + + ranger.pdp.authn.kerberos.token.valid.seconds + 3600 + Validity period (seconds) for Kerberos authentication tokens. + + + + ranger.pdp.authn.kerberos.name.rules + DEFAULT + Rules for mapping Kerberos principal names to short usernames. + + + + 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..5ed3cadd35 --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatsTest.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RangerPdpStatsTest { + @Test + public void testRequestCountersAndAverageLatency() { + RangerPdpStats stats = new RangerPdpStats(); + + stats.recordRequestSuccess(5_000_000L); // 5 ms + stats.recordRequestBadRequest(15_000_000L); // 15 ms + stats.recordRequestError(10_000_000L); // 10 ms + stats.recordAuthFailure(10_000_000L); + + assertEquals(4L, stats.getTotalRequests()); + assertEquals(1L, stats.getTotalAuthzSuccess()); + assertEquals(1L, stats.getTotalAuthzBadRequest()); + assertEquals(1L, stats.getTotalAuthzErrors()); + assertEquals(1L, stats.getTotalAuthFailures()); + assertEquals(40_000_000L, stats.getTotalLatencyNanos()); + assertEquals(10L, stats.getAverageLatencyMs()); + } + + @Test + public void testNegativeLatencyIsClampedToZero() { + RangerPdpStats stats = new RangerPdpStats(); + + stats.recordRequestSuccess(-100L); + + assertEquals(0L, stats.getTotalLatencyNanos()); + assertEquals(0L, stats.getAverageLatencyMs()); + } +} diff --git a/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java new file mode 100644 index 0000000000..3b8f16c748 --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/RangerPdpStatusServletTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ranger.pdp; + +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RangerPdpStatusServletTest { + @AfterEach + public void clearOverrides() { + System.clearProperty(RangerPdpConstants.PROP_AUTHZ_POLICY_CACHE_DIR); + } + + @Test + public void testMetricsEndpointRendersCounters() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + + stats.recordRequestSuccess(5_000_000L); + stats.recordRequestError(5_000_000L); + stats.recordAuthFailure(5_000_000L); + + RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(stats, RangerPdpStatusServlet.Mode.METRICS); + HttpServletRequest req = proxy(HttpServletRequest.class, (proxy, method, args) -> null); + ResponseCapture capture = new ResponseCapture(); + HttpServletResponse resp = capture.responseProxy(); + + servlet.doGet(req, resp); + + assertEquals(HttpServletResponse.SC_OK, capture.status); + assertTrue(capture.body.toString().contains("ranger_pdp_requests_total 3")); + assertTrue(capture.body.toString().contains("ranger_pdp_auth_failures_total 1")); + } + + @Test + public void testLoadedServicesCount() throws Exception { + RangerPdpStatusServlet servlet = new RangerPdpStatusServlet(new RangerPdpStats(), RangerPdpStatusServlet.Mode.READY); + HttpServletRequest req = proxy(HttpServletRequest.class, (proxy, method, args) -> null); + Method method = RangerPdpStatusServlet.class.getDeclaredMethod("getLoadedServicesCount", HttpServletRequest.class); + + method.setAccessible(true); + + Integer servicesCount = (Integer) method.invoke(servlet, req); + + assertTrue(servicesCount >= 0L); + } + + @SuppressWarnings("unchecked") + private static T 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..79119171bc --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/config/RangerPdpConfigTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.config; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RangerPdpConfigTest { + @AfterEach + public void clearSystemOverrides() { + System.clearProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME); + System.clearProperty(RangerPdpConstants.PROP_PORT); + } + + @Test + public void testHeaderUserNameCanBeOverriddenBySystemProperty() { + System.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Test-User"); + + RangerPdpConfig config = new RangerPdpConfig(); + + assertEquals("X-Test-User", config.getHeaderAuthnUsername()); + } + + @Test + public void testInvalidPortFallsBackToDefault() { + System.setProperty(RangerPdpConstants.PROP_PORT, "not-a-number"); + + RangerPdpConfig config = new RangerPdpConfig(); + + assertEquals(6500, config.getPort()); + } +} diff --git a/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java b/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java new file mode 100644 index 0000000000..1221af3bbd --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/rest/RangerPdpRESTTest.java @@ -0,0 +1,360 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.rest; + +import org.apache.ranger.authz.api.RangerAuthorizer; +import org.apache.ranger.authz.api.RangerAuthzApiErrorCode; +import org.apache.ranger.authz.api.RangerAuthzException; +import org.apache.ranger.authz.model.RangerAccessContext; +import org.apache.ranger.authz.model.RangerAuthzRequest; +import org.apache.ranger.authz.model.RangerAuthzResult; +import org.apache.ranger.authz.model.RangerMultiAuthzRequest; +import org.apache.ranger.authz.model.RangerMultiAuthzResult; +import org.apache.ranger.authz.model.RangerResourcePermissions; +import org.apache.ranger.authz.model.RangerResourcePermissionsRequest; +import org.apache.ranger.authz.model.RangerUserInfo; +import org.apache.ranger.pdp.RangerPdpStats; +import org.apache.ranger.pdp.config.RangerPdpConfig; +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.junit.jupiter.api.Test; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Response; + +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_PDP_SERVICE_PREFIX; +import static org.apache.ranger.pdp.config.RangerPdpConstants.PROP_SUFFIX_DELEGATION_USERS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RangerPdpRESTTest { + private static final String CALLER_DELEGATION_ALLOWED = "alice"; + private static final String CALLER_DELEGATION_NOT_ALLOWED = "bob"; + private static final RangerUserInfo USER_USER1 = new RangerUserInfo("user1"); + private static final RangerAccessContext CONTEXT_SERVICE_SVC1 = new RangerAccessContext("hive", "svc1"); + + @Test + public void testAuthorizeReturnsUnauthorizedWhenCallerMissing() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(); + RangerPdpREST rest = createRest(authorizer, stats); + RangerAuthzRequest request = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1); + Response response = rest.authorize(request, httpRequest(null, stats)); + + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthFailures()); + } + + @Test + public void testAuthorizeReturnsForbiddenWhenCallerNotAllowed() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(); + RangerPdpREST rest = createRest(authorizer, stats); + RangerAuthzRequest request = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1); + Response response = rest.authorize(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats)); + + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthFailures()); + } + + @Test + public void testAuthorizeReturnsOkAndRecordsSuccess() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(new RangerAuthzResult("req-1", RangerAuthzResult.AccessDecision.ALLOW)); + RangerPdpREST rest = createRest(authorizer, stats); + RangerAuthzRequest request = new RangerAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1); + Response response = rest.authorize(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats)); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalRequests()); + assertEquals(1L, stats.getTotalAuthzSuccess()); + } + + @Test + public void testAuthorizeReturnsBadRequestOnAuthzException() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING)); + RangerPdpREST rest = createRest(authorizer, stats); + RangerAuthzRequest request = new RangerAuthzRequest(USER_USER1, null, CONTEXT_SERVICE_SVC1); + Response response = rest.authorize(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats)); + + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthzBadRequest()); + } + + @Test + public void testAuthorizeMultiReturnsUnauthorizedWhenCallerMissing() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(); + RangerPdpREST rest = createRest(authorizer, stats); + RangerMultiAuthzRequest request = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1); + Response response = rest.authorizeMulti(request, httpRequest(null, stats)); + + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthFailures()); + } + + @Test + public void testAuthorizeMultiReturnsForbiddenWhenCallerNotAllowed() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(); + RangerPdpREST rest = createRest(authorizer, stats); + RangerMultiAuthzRequest request = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1); + Response response = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats)); + + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthFailures()); + } + + @Test + public void testAuthorizeMultiReturnsOkAndRecordsSuccess() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(new RangerMultiAuthzResult("req-1", RangerAuthzResult.AccessDecision.ALLOW)); + RangerPdpREST rest = createRest(authorizer, stats); + RangerMultiAuthzRequest request = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1); + Response response = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats)); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalRequests()); + assertEquals(1L, stats.getTotalAuthzSuccess()); + } + + @Test + public void testAuthorizeMultiReturnsBadRequestOnAuthzException() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING)); + RangerPdpREST rest = createRest(authorizer, stats); + RangerMultiAuthzRequest request = new RangerMultiAuthzRequest("req-1", USER_USER1, null, CONTEXT_SERVICE_SVC1); + Response response = rest.authorizeMulti(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats)); + + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthzBadRequest()); + } + + @Test + public void testGetResourcePermissionsReturnsUnauthorizedWhenCallerMissing() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(); + RangerPdpREST rest = createRest(authorizer, stats); + RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1); + Response response = rest.getResourcePermissions(request, httpRequest(null, stats)); + + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthFailures()); + } + + @Test + public void testGetResourcePermissionsReturnsForbiddenWhenCallerNotAllowed() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(); + RangerPdpREST rest = createRest(authorizer, stats); + RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1); + Response response = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_NOT_ALLOWED, stats)); + + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthFailures()); + } + + @Test + public void testGetResourcePermissionsReturnsOkAndRecordsSuccess() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(new RangerResourcePermissions()); + RangerPdpREST rest = createRest(authorizer, stats); + RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1); + Response response = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats)); + + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalRequests()); + assertEquals(1L, stats.getTotalAuthzSuccess()); + } + + @Test + public void testGetResourcePermissionsReturnsBadRequestOnAuthzException() throws Exception { + RangerPdpStats stats = new RangerPdpStats(); + TestAuthorizer authorizer = new TestAuthorizer(new RangerAuthzException(RangerAuthzApiErrorCode.INVALID_REQUEST_ACCESS_CONTEXT_MISSING)); + RangerPdpREST rest = createRest(authorizer, stats); + RangerResourcePermissionsRequest request = new RangerResourcePermissionsRequest("req-1", null, CONTEXT_SERVICE_SVC1); + Response response = rest.getResourcePermissions(request, httpRequest(CALLER_DELEGATION_ALLOWED, stats)); + + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals(1L, stats.getTotalAuthzBadRequest()); + } + + private static RangerPdpREST createRest(TestAuthorizer authorizer, RangerPdpStats stats) throws Exception { + RangerPdpREST rest = new RangerPdpREST(); + + setField(rest, "authorizer", authorizer); + setField(rest, "config", new TestConfig(defaultConfig())); + setField(rest, "servletContext", servletContextWithStats(stats)); + rest.initialize(); + + return rest; + } + + private static HttpServletRequest httpRequest(String user, RangerPdpStats stats) { + Map 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, CALLER_DELEGATION_ALLOWED); + + return ret; + } + + private static class TestConfig extends RangerPdpConfig { + private final Properties props; + + TestConfig(Properties props) { + this.props = props; + } + + @Override + public Properties getAuthzProperties() { + return new Properties(props); + } + } + + private static class TestAuthorizer extends RangerAuthorizer { + private final RangerAuthzResult authzResult; + private final RangerMultiAuthzResult multiAuthzResult; + private final RangerResourcePermissions permissions; + private final RangerAuthzException authzException; + + TestAuthorizer() { + this(null, null, null, null); + } + + TestAuthorizer(RangerAuthzResult result) { + this(result, null, null, null); + } + + TestAuthorizer(RangerMultiAuthzResult result) { + this(null, result, null, null); + } + + TestAuthorizer(RangerResourcePermissions permissions) { + this(null, null, permissions, null); + } + + TestAuthorizer(RangerAuthzException excp) { + this(null, null, null, excp); + } + + TestAuthorizer(RangerAuthzResult result, RangerMultiAuthzResult multiResult, RangerResourcePermissions permissions, RangerAuthzException excp) { + super(new Properties()); + + this.authzResult = result; + this.multiAuthzResult = multiResult; + this.permissions = permissions; + this.authzException = excp; + } + + @Override + public void init() { + } + + @Override + public void close() { + } + + @Override + public RangerAuthzResult authorize(RangerAuthzRequest request) throws RangerAuthzException { + if (authzException != null) { + throw authzException; + } + + return authzResult != null ? authzResult : new RangerAuthzResult(); + } + + @Override + public RangerMultiAuthzResult authorize(RangerMultiAuthzRequest request) throws RangerAuthzException { + if (authzException != null) { + throw authzException; + } + + return multiAuthzResult != null ? multiAuthzResult : new RangerMultiAuthzResult(); + } + + @Override + public RangerResourcePermissions getResourcePermissions(RangerResourcePermissionsRequest request) throws RangerAuthzException { + if (authzException != null) { + throw authzException; + } + + return permissions != null ? permissions : new RangerResourcePermissions(); + } + } +} diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java new file mode 100644 index 0000000000..c497be9086 --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.junit.jupiter.api.Test; + +import javax.servlet.http.HttpServletRequest; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HttpHeaderAuthNHandlerTest { + @Test + public void testAuthenticate_usesDefaultHeaderName() { + HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler(); + Properties config = new Properties(); + + handler.init(config); + + HttpServletRequest request = requestWithHeader("X-Forwarded-User", "alice"); + PdpAuthNHandler.Result result = handler.authenticate(request, null); + + assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus()); + assertEquals("alice", result.getUserName()); + assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE, result.getAuthType()); + } + + @Test + public void testAuthenticate_usesConfiguredHeaderName() { + HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler(); + Properties config = new Properties(); + + config.setProperty(RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME, "X-Authenticated-User"); + + handler.init(config); + + HttpServletRequest request = requestWithHeader("X-Authenticated-User", "bob"); + PdpAuthNHandler.Result result = handler.authenticate(request, null); + + assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus()); + assertEquals("bob", result.getUserName()); + } + + private static HttpServletRequest requestWithHeader(String expectedHeader, String headerValue) { + InvocationHandler invocationHandler = (proxy, method, args) -> { + String methodName = method.getName(); + + if ("getHeader".equals(methodName)) { + return expectedHeader.equals(args[0]) ? headerValue : null; + } else if ("getHeaders".equals(methodName) || "getHeaderNames".equals(methodName)) { + return java.util.Collections.emptyEnumeration(); + } else if ("getMethod".equals(methodName)) { + return "POST"; + } else if ("toString".equals(methodName)) { + return "HttpServletRequest(test)"; + } + + return null; + }; + + return (HttpServletRequest) Proxy.newProxyInstance(HttpServletRequest.class.getClassLoader(), new Class[] {HttpServletRequest.class}, invocationHandler); + } +} diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java new file mode 100644 index 0000000000..891cd2bb84 --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/security/KerberosAuthHandlerTest.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +import org.apache.hadoop.security.authentication.util.KerberosName; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class KerberosAuthHandlerTest { + @AfterEach + public void restoreDefaultRules() { + KerberosName.setRules("DEFAULT"); + } + + @Test + public void testApplyNameRules_usesConfiguredRules() throws Exception { + KerberosAuthNHandler handler = new KerberosAuthNHandler(); + + invokePrivateVoid(handler, "initializeKerberosNameRules", new Class[] {String.class}, "DEFAULT"); + + String shortName = (String) invokePrivate(handler, "applyNameRules", new Class[] {String.class}, "alice@EXAMPLE.COM"); + + assertEquals("alice", shortName); + } + + @Test + public void testApplyNameRules_fallsBackForInvalidPrincipal() throws Exception { + KerberosAuthNHandler handler = new KerberosAuthNHandler(); + + invokePrivateVoid(handler, "initializeKerberosNameRules", new Class[] {String.class}, "DEFAULT"); + + String shortName = (String) invokePrivate(handler, "applyNameRules", new Class[] {String.class}, "svc/host"); + + assertEquals("svc", shortName); + } + + private static void invokePrivateVoid(Object target, String methodName, Class[] paramTypes, Object... args) throws Exception { + invokePrivate(target, methodName, paramTypes, args); + } + + private static Object invokePrivate(Object target, String methodName, Class[] paramTypes, Object... args) throws Exception { + Method method = target.getClass().getDeclaredMethod(methodName, paramTypes); + + method.setAccessible(true); + + return method.invoke(target, args); + } +} diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java new file mode 100644 index 0000000000..0d0e183c55 --- /dev/null +++ b/pdp/src/test/java/org/apache/ranger/pdp/security/RangerPdpAuthNFilterTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.ranger.pdp.security; + +import org.apache.ranger.pdp.config.RangerPdpConstants; +import org.junit.jupiter.api.Test; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class RangerPdpAuthNFilterTest { + @Test + public void testInit_skipsHeaderHandlerWhenDisabled() { + RangerPdpAuthNFilter filter = new RangerPdpAuthNFilter(); + Map params = new HashMap<>(); + + params.put(RangerPdpConstants.PROP_AUTHN_TYPES, "header"); + params.put(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, "false"); + + assertThrows(ServletException.class, () -> filter.init(new TestFilterConfig(params))); + } + + @Test + public void testInit_registersHeaderHandlerWhenEnabled() throws Exception { + RangerPdpAuthNFilter filter = new RangerPdpAuthNFilter(); + Map params = new HashMap<>(); + + params.put(RangerPdpConstants.PROP_AUTHN_TYPES, "header"); + params.put(RangerPdpConstants.PROP_AUTHN_HEADER_ENABLED, "true"); + + filter.init(new TestFilterConfig(params)); + + Field handlersField = RangerPdpAuthNFilter.class.getDeclaredField("handlers"); + + handlersField.setAccessible(true); + + @SuppressWarnings("unchecked") + List handlers = (List) handlersField.get(filter); + + assertEquals(1, handlers.size()); + assertEquals(HttpHeaderAuthNHandler.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 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()