Copilot commented on code in PR #896:
URL: https://github.com/apache/ranger/pull/896#discussion_r3004611903


##########
pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java:
##########
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.pdp.security;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * Servlet filter that enforces authentication for all PDP REST endpoints.
+ *
+ * <p>Handlers are configured via the {@code ranger.pdp.auth.types} filter 
init parameter
+ * (comma-separated list of {@code header}, {@code jwt}, {@code kerberos}).  
Handlers are
+ * tried in the listed order; the first successful match wins.
+ *
+ * <p>On success the authenticated username is stored in the request attribute
+ * {@link RangerPdpConstants#ATTR_AUTHENTICATED_USER} so that REST resources 
can read it.
+ *
+ * <p>If all handlers return {@code SKIP} (no recognisable credentials found), 
the filter
+ * sends a {@code 401} response with {@code WWW-Authenticate} headers for every
+ * configured handler that provides a challenge.
+ */
+public class RangerPdpAuthFilter implements Filter {
+    private static final Logger LOG = 
LoggerFactory.getLogger(RangerPdpAuthFilter.class);
+
+    // Auth type list — pdp-specific
+    public static final String PARAM_AUTH_TYPES = 
RangerPdpConstants.PROP_AUTH_TYPES;
+
+    // HTTP Header auth — consistent with ranger.admin.authn.header.* in 
security-admin
+    public static final String PARAM_HEADER_AUTHN_ENABLED  = 
RangerPdpConstants.PROP_HEADER_AUTHN_ENABLED;
+    public static final String PARAM_HEADER_AUTHN_USERNAME = 
RangerPdpConstants.PROP_HEADER_AUTHN_USERNAME;
+
+    // JWT bearer token auth
+    public static final String PARAM_JWT_PROVIDER_URL = 
RangerPdpConstants.PROP_JWT_PROVIDER_URL;
+    public static final String PARAM_JWT_PUBLIC_KEY   = 
RangerPdpConstants.PROP_JWT_PUBLIC_KEY;
+    public static final String PARAM_JWT_COOKIE_NAME  = 
RangerPdpConstants.PROP_JWT_COOKIE_NAME;
+    public static final String PARAM_JWT_AUDIENCES    = 
RangerPdpConstants.PROP_JWT_AUDIENCES;
+
+    // Kerberos / SPNEGO
+    public static final String PARAM_SPNEGO_PRINCIPAL   = 
RangerPdpConstants.PROP_SPNEGO_PRINCIPAL;
+    public static final String PARAM_SPNEGO_KEYTAB      = 
RangerPdpConstants.PROP_SPNEGO_KEYTAB;
+    public static final String PARAM_KRB_NAME_RULES     = 
RangerPdpConstants.PROP_KRB_NAME_RULES;
+    public static final String PARAM_KRB_TOKEN_VALIDITY = 
RangerPdpConstants.PROP_KRB_TOKEN_VALIDITY;
+    public static final String PARAM_KRB_COOKIE_DOMAIN  = 
RangerPdpConstants.PROP_KRB_COOKIE_DOMAIN;
+    public static final String PARAM_KRB_COOKIE_PATH    = 
RangerPdpConstants.PROP_KRB_COOKIE_PATH;
+
+    private final List<PdpAuthHandler> handlers = new ArrayList<>();
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+        Properties config            = toProperties(filterConfig);
+        String     types             = 
filterConfig.getInitParameter(PARAM_AUTH_TYPES);
+        boolean    headerAuthEnabled = 
Boolean.parseBoolean(StringUtils.defaultIfBlank(filterConfig.getInitParameter(PARAM_HEADER_AUTHN_ENABLED),
 "false"));
+
+        if (StringUtils.isBlank(types)) {
+            types = "header,jwt,kerberos";
+        }
+
+        for (String type : types.split(",")) {
+            String normalizedType = type.trim().toLowerCase();
+
+            if ("header".equals(normalizedType) && !headerAuthEnabled) {
+                LOG.info("Header auth type is configured but disabled via 
{}=false; skipping handler registration", PARAM_HEADER_AUTHN_ENABLED);
+                continue;
+            }
+
+            PdpAuthHandler handler = createHandler(normalizedType);
+
+            if (handler != null) {
+                try {
+                    handler.init(config);
+                    handlers.add(handler);
+
+                    LOG.info("Registered auth handler: {}", normalizedType);
+                } catch (Exception e) {
+                    throw new ServletException("Failed to initialize auth 
handler: " + type, e);
+                }
+            } else {
+                LOG.warn("Unknown auth type ignored: {}", type.trim());
+            }
+        }

Review Comment:
   `init()` fails the entire filter if any configured handler throws during 
initialization. With the shipped defaults (`ranger.pdp.auth.types=jwt,kerberos` 
and empty Kerberos principal/keytab), `KerberosAuthHandler.init()` will throw 
and the PDP server won't start even though JWT could work. Consider treating 
missing/invalid per-handler config as “handler disabled” (log + continue) and 
only fail if *no* handlers can be registered, or adjust defaults so Kerberos 
isn't enabled unless configured.



##########
pdp/src/main/resources/ranger-pdp-default.xml:
##########
@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<configuration>
+  <property>
+    <name>ranger.pdp.port</name>
+    <value>6500</value>
+    <description>Port the PDP server listens on.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.log.dir</name>
+    <value>/var/log/ranger/pdp</value>
+    <description>Directory for PDP server log files.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.*.delegation.users</name>
+    <value/>
+    <description>Comma-separated users, allowed to call on behalf of other 
users in all services</description>
+  </property>
+
+  <!-- SSL/TLS -->
+  <property>
+    <name>ranger.pdp.ssl.enabled</name>
+    <value>false</value>
+    <description>Set to true to enable HTTPS.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.file</name>
+    <value/>
+    <description>Path to the keystore file (required when SSL is 
enabled).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.password</name>
+    <value/>
+    <description>Keystore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.type</name>
+    <value>JKS</value>
+    <description>Keystore type (JKS, PKCS12).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.enabled</name>
+    <value>false</value>
+    <description>Set to true to require client certificate 
authentication.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.file</name>
+    <value/>
+    <description>Path to the truststore file.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.password</name>
+    <value/>
+    <description>Truststore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.type</name>
+    <value>JKS</value>
+    <description>Truststore type (JKS, PKCS12).</description>
+  </property>
+
+  <!-- HTTP/2 -->
+  <property>
+    <name>ranger.pdp.http2.enabled</name>
+    <value>true</value>
+    <description>
+      Enable HTTP/2 via upgrade on the connector.
+      Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside 
HTTP/1.1.
+    </description>
+  </property>
+
+  <!-- Connector concurrency / queue limits -->
+  <property>
+    <name>ranger.pdp.http.connector.maxThreads</name>
+    <value>200</value>
+    <description>Maximum number of worker threads handling simultaneous 
requests.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.minSpareThreads</name>
+    <value>20</value>
+    <description>Minimum number of spare worker threads kept 
ready.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.acceptCount</name>
+    <value>100</value>
+    <description>Queued connection backlog when all worker threads are 
busy.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.maxConnections</name>
+    <value>10000</value>
+    <description>Maximum concurrent TCP connections accepted by the 
connector.</description>
+  </property>
+
+  <!-- Authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.auth.types</name>
+    <value>jwt,kerberos</value>
+    <description>
+      Comma-separated list of authentication methods for incoming REST 
requests,
+      tried in listed order. Supported values: header, jwt, kerberos.
+    </description>
+  </property>
+
+  <!-- HTTP Header authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.authn.header.enabled</name>
+    <value>false</value>
+    <description>
+      Enable trusted HTTP header authentication. Use only behind a trusted 
proxy.
+    </description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.authn.header.username</name>
+    <value>X-Forwarded-User</value>
+    <description>HTTP header name from which the authenticated username is 
read.</description>
+  </property>
+
+  <!-- JWT bearer token authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.jwt.provider.url</name>
+    <value/>
+    <description>URL of the JWT provider (used to fetch the public 
key).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.jwt.public.key</name>
+    <value/>
+    <description>Base64-encoded public key for verifying JWT 
signatures.</description>
+  </property>

Review Comment:
   The description for `ranger.pdp.jwt.public.key` says "Base64-encoded public 
key", but the underlying Ranger JWT handler expects a PEM/X.509 formatted value 
(see `RangerJwtAuthHandler` using `X509CertUtils.parse(pemPublicKey)`). Please 
align this description with the actual expected format to avoid 
misconfiguration.



##########
pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java:
##########
@@ -0,0 +1,494 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.pdp.rest;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.authz.api.RangerAuthzException;
+import org.apache.ranger.authz.model.RangerAccessInfo;
+import org.apache.ranger.authz.model.RangerAuthzRequest;
+import org.apache.ranger.authz.model.RangerAuthzResult;
+import org.apache.ranger.authz.model.RangerMultiAuthzRequest;
+import org.apache.ranger.authz.model.RangerMultiAuthzResult;
+import org.apache.ranger.authz.model.RangerResourceInfo;
+import org.apache.ranger.authz.model.RangerResourcePermissions;
+import org.apache.ranger.authz.model.RangerResourcePermissionsRequest;
+import org.apache.ranger.authz.model.RangerUserInfo;
+import org.apache.ranger.pdp.RangerPdpStats;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static 
org.apache.ranger.pdp.config.RangerPdpConstants.PROP_PDP_SERVICE_PREFIX;
+import static 
org.apache.ranger.pdp.config.RangerPdpConstants.PROP_SUFFIX_DELEGATION_USERS;
+import static 
org.apache.ranger.pdp.config.RangerPdpConstants.WILDCARD_SERVICE_NAME;
+
+/**
+ * REST resource that exposes the three core {@link RangerAuthorizer} methods 
over HTTP.
+ *
+ * <p>All endpoints are under {@code /authz/v1} and produce/consume {@code 
application/json}.
+ * Authentication is enforced upstream by {@link RangerPdpAuthFilter}; the 
authenticated
+ * caller's identity is read from the {@link 
RangerPdpConstants#ATTR_AUTHENTICATED_USER}
+ * request attribute.
+ *
+ * <table border="1">
+ *   <tr><th>Method</th><th>Path</th><th>Request body</th><th>Response 
body</th></tr>
+ *   <tr><td>POST</td><td>/authz/v1/authorize</td>
+ *       <td>{@link RangerAuthzRequest}</td><td>{@link 
RangerAuthzResult}</td></tr>
+ *   <tr><td>POST</td><td>/authz/v1/authorizeMulti</td>
+ *       <td>{@link RangerMultiAuthzRequest}</td><td>{@link 
RangerMultiAuthzResult}</td></tr>
+ *   <tr><td>POST</td><td>/authz/v1/permissions</td>
+ *       <td>{@link RangerResourcePermissionsRequest}</td>
+ *       <td>{@link RangerResourcePermissions}</td></tr>
+ * </table>
+ */
+@Path("/v1")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+@Singleton
+public class RangerPdpREST {
+    private static final Logger LOG = 
LoggerFactory.getLogger(RangerPdpREST.class);
+
+    private static final Response RESPONSE_OK = Response.ok().build();
+
+    private final Map<String, Set<String>> delegationUsersByService = new 
HashMap<>();
+
+    @Inject
+    private RangerAuthorizer authorizer;
+
+    @Inject
+    private RangerPdpConfig config;
+
+    @Context
+    private ServletContext servletContext;
+
+    @PostConstruct
+    public void initialize() {
+        initializeDelegationUsers();
+    }
+
+    /**
+     * Evaluates a single access request.
+     *
+     * @param request the authorization request
+     * @return {@code 200 OK} with {@link RangerAuthzResult}, or {@code 400} / 
{@code 500} on error
+     */
+    @POST
+    @Path("/authorize")
+    public Response authorize(RangerAuthzRequest request, @Context 
HttpServletRequest httpRequest) {
+        long     startNanos = System.nanoTime();
+        Response ret        = null;
+
+        try {
+            String           requestId   = request != null ? 
request.getRequestId() : null;
+            String           caller      = getAuthenticatedUser(httpRequest);
+            String           serviceName = getServiceName(request);
+            RangerUserInfo   user        = request != null ? request.getUser() 
: null;
+            RangerAccessInfo access      = request != null ? 
request.getAccess() : null;
+
+            LOG.debug("==> authorize(requestId={}, caller={}, 
serviceName={})", requestId, caller, serviceName);
+
+            ret = validateCaller(caller, user, access, serviceName);
+
+            if (RESPONSE_OK.equals(ret)) {
+                try {
+                    RangerAuthzResult result = authorizer.authorize(request);
+
+                    ret = Response.ok(result).build();
+                } catch (RangerAuthzException e) {
+                    LOG.warn("authorize(requestId={}): authorization error; 
caller={}", requestId, caller, e);
+
+                    ret = badRequest(e);
+                } catch (Exception e) {
+                    LOG.error("authorize(requestId={}): internal error; 
caller={}", requestId, caller, e);
+
+                    ret = serverError(e);
+                }
+            }
+
+            LOG.debug("<== authorize(requestId={}, caller={}, serviceName={}): 
ret={}", requestId, caller, serviceName, ret != null ? ret.getStatus() : null);
+        } finally {
+            recordRequestMetrics(ret, startNanos, httpRequest);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Evaluates multiple access requests in a single call.
+     *
+     * @param request the multi-access authorization request
+     * @return {@code 200 OK} with {@link RangerMultiAuthzResult}, or {@code 
400} / {@code 500} on error
+     */
+    @POST
+    @Path("/authorizeMulti")
+    public Response authorizeMulti(RangerMultiAuthzRequest request, @Context 
HttpServletRequest httpRequest) {
+        long     startNanos = System.nanoTime();
+        Response ret        = null;
+
+        try {
+            String                 requestId   = request != null ? 
request.getRequestId() : null;
+            String                 caller      = 
getAuthenticatedUser(httpRequest);
+            String                 serviceName = getServiceName(request);
+            RangerUserInfo         user        = request != null ? 
request.getUser() : null;
+            List<RangerAccessInfo> accesses    = request != null ? 
request.getAccesses() : null;
+
+            LOG.debug("==> authorizeMulti(requestId={}, caller={}, 
serviceName={})", requestId, caller, serviceName);
+
+            ret = validateCaller(caller, user, accesses, serviceName);
+
+            if (RESPONSE_OK.equals(ret)) {
+                try {
+                    RangerMultiAuthzResult result = 
authorizer.authorize(request);
+
+                    ret = Response.ok(result).build();
+                } catch (RangerAuthzException e) {
+                    LOG.warn("authorizeMulti(requestId={}): authorization 
error; caller={}", requestId, caller, e);
+
+                    ret = badRequest(e);
+                } catch (Exception e) {
+                    LOG.error("authorizeMulti(requestId={}): internal error; 
caller={}", requestId, caller, e);
+
+                    ret = serverError(e);
+                }
+            }
+
+            LOG.debug("<== authorizeMulti(requestId={}, caller={}, 
serviceName={}): ret={}", requestId, caller, serviceName, ret != null ? 
ret.getStatus() : null);
+        } finally {
+            recordRequestMetrics(ret, startNanos, httpRequest);
+        }
+
+        return ret;
+    }
+
+    /**
+     * Returns the effective permissions for a resource, broken down by 
user/group/role.
+     *
+     * @param request wrapper containing the resource info and access context
+     * @return {@code 200 OK} with {@link RangerResourcePermissions}, or 
{@code 400} / {@code 500} on error
+     */
+    @POST
+    @Path("/permissions")
+    public Response getResourcePermissions(RangerResourcePermissionsRequest 
request, @Context HttpServletRequest httpRequest) {
+        long     startNanos = System.nanoTime();
+        Response ret        = null;
+
+        try {
+            String caller      = getAuthenticatedUser(httpRequest);
+            String serviceName = getServiceName(request);
+
+            LOG.debug("==> getResourcePermissions(caller={}, serviceName={})", 
caller, serviceName);
+
+            ret = validateCaller(caller, serviceName);
+
+            if (RESPONSE_OK.equals(ret)) {
+                try {
+                    RangerResourcePermissions result = 
authorizer.getResourcePermissions(request);
+
+                    ret = Response.ok(result).build();
+                } catch (RangerAuthzException e) {
+                    LOG.warn("getResourcePermissions(): validation error; 
caller={}", caller, e);
+
+                    ret = badRequest(e);
+                } catch (Exception e) {
+                    LOG.error("getResourcePermissions(): unexpected error; 
caller={}", caller, e);
+
+                    ret = serverError(e);
+                }
+            }
+
+            LOG.debug("<== getResourcePermissions(caller={}, serviceName={}): 
ret={}", caller, serviceName, ret != null ? ret.getStatus() : null);
+        } finally {
+            recordRequestMetrics(ret, startNanos, httpRequest);
+        }
+
+        return ret;
+    }
+
+    private String getAuthenticatedUser(HttpServletRequest httpRequest) {
+        Object user = 
httpRequest.getAttribute(RangerPdpConstants.ATTR_AUTHENTICATED_USER);
+
+        return user != null ? user.toString() : null;
+    }
+
+    private static String getServiceName(RangerAuthzRequest request) {
+        return request != null && request.getContext() != null ? 
request.getContext().getServiceName() : null;
+    }
+
+    private static String getServiceName(RangerMultiAuthzRequest request) {
+        return request != null && request.getContext() != null ? 
request.getContext().getServiceName() : null;
+    }
+
+    private static String getServiceName(RangerResourcePermissionsRequest 
request) {
+        return request != null && request.getContext() != null ? 
request.getContext().getServiceName() : null;
+    }
+
+    private Response validateCaller(String caller, RangerUserInfo user, 
RangerAccessInfo access, String serviceName) {
+        final Response ret;
+
+        if (StringUtils.isBlank(caller)) {
+            ret = Response.status(Response.Status.UNAUTHORIZED)
+                    .entity(new ErrorResponse(Response.Status.UNAUTHORIZED, 
"Authentication required"))
+                    .build();
+        } else {
+            boolean needsDelegation = isDelegationNeeded(caller, user) || 
isDelegationNeeded(access);
+
+            if (needsDelegation) {
+                if (!isDelegationUserForService(serviceName, caller)) {
+                    LOG.info("{} is not a delegation user in service {}", 
caller, serviceName);
+
+                    ret = Response.status(Response.Status.FORBIDDEN)
+                            .entity(new 
ErrorResponse(Response.Status.FORBIDDEN, caller + " is not authorized"))
+                            .build();
+                } else {
+                    ret = RESPONSE_OK;
+                }
+            } else {
+                ret = RESPONSE_OK;
+            }
+        }
+
+        return ret;
+    }
+
+    private Response validateCaller(String caller, RangerUserInfo user, 
List<RangerAccessInfo> accesses, String serviceName) {
+        final Response ret;
+
+        if (StringUtils.isBlank(caller)) {
+            ret = Response.status(Response.Status.UNAUTHORIZED)
+                    .entity(new ErrorResponse(Response.Status.UNAUTHORIZED, 
"Authentication required"))
+                    .build();
+        } else {
+            boolean needsDelegation = isDelegationNeeded(caller, user) || 
isDelegationNeeded(accesses);
+
+            if (needsDelegation) {
+                if (!isDelegationUserForService(serviceName, caller)) {
+                    LOG.info("{} is not a delegation user in service {}", 
caller, serviceName);
+
+                    ret = Response.status(Response.Status.FORBIDDEN)
+                            .entity(new 
ErrorResponse(Response.Status.FORBIDDEN, caller + " is not authorized"))
+                            .build();
+                } else {
+                    ret = RESPONSE_OK;
+                }
+            } else {
+                ret = RESPONSE_OK;
+            }
+        }
+
+        return ret;
+    }
+
+    private Response validateCaller(String caller, String serviceName) {
+        final Response ret;
+
+        if (StringUtils.isBlank(caller)) {
+            ret = Response.status(Response.Status.UNAUTHORIZED)
+                    .entity(new ErrorResponse(Response.Status.UNAUTHORIZED, 
"Authentication required"))
+                    .build();
+        } else if (!isDelegationUserForService(serviceName, caller)) {
+            LOG.info("{} is not a delegation user in service {}", caller, 
serviceName);
+
+            ret = Response.status(Response.Status.FORBIDDEN)
+                    .entity(new ErrorResponse(Response.Status.FORBIDDEN, 
caller + " is not authorized"))
+                    .build();
+        } else {
+            ret = RESPONSE_OK;
+        }
+
+        return ret;
+    }
+
+    private boolean isDelegationNeeded(String caller, RangerUserInfo user) {
+        String  userName        = user != null ? user.getName() : null;
+        boolean needsDelegation = !caller.equals(userName);
+
+        if (!needsDelegation) {
+            // don't trust user-attributes/groups/roles if caller doesn't have 
delegation permission
+            needsDelegation = MapUtils.isNotEmpty(user.getAttributes()) || 
CollectionUtils.isNotEmpty(user.getGroups()) || 
CollectionUtils.isNotEmpty(user.getRoles());
+        }
+
+        return needsDelegation;
+    }
+
+    private boolean isDelegationNeeded(RangerAccessInfo access) {
+        RangerResourceInfo resource = access != null ? access.getResource() : 
null;
+
+        // delegation permission is needed when resource attributes are 
specified
+        return (resource != null && 
MapUtils.isNotEmpty(resource.getAttributes()));
+    }
+
+    private boolean isDelegationNeeded(List<RangerAccessInfo> accesses) {
+        if (accesses != null) {
+            for (RangerAccessInfo access : accesses) {
+                if (isDelegationNeeded(access)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    private RangerPdpStats getRuntimeState(HttpServletRequest httpRequest) {
+        Object state = 
httpRequest.getServletContext().getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE);
+
+        if (state instanceof RangerPdpStats) {
+            return (RangerPdpStats) state;
+        } else {
+            Object fallback = servletContext != null ? 
servletContext.getAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE) 
: null;
+
+            return (fallback instanceof RangerPdpStats) ? (RangerPdpStats) 
fallback : new RangerPdpStats();
+        }
+    }
+
+    private void recordRequestMetrics(Response ret, long startNanos, 
HttpServletRequest httpRequest) {
+        RangerPdpStats state   = getRuntimeState(httpRequest);
+        int            status  = ret != null ? ret.getStatus() : 500;
+        long           elapsed = System.nanoTime() - startNanos;
+
+        if (status >= 200 && status < 300) {
+            state.recordRequestSuccess(elapsed);
+        } else if (status == 401 || status == 403) { // UNAUTHORIZED or 
FORBIDDEN
+            state.recordAuthFailure();
+        } else if (status == 400) {
+            state.recordRequestBadRequest(elapsed);
+        } else {
+            state.recordRequestError(elapsed);
+        }
+    }
+
+    private boolean isDelegationUserForService(String serviceName, String 
userName) {
+        final boolean            ret;
+        Map<String, Set<String>> delegationUsersByService = 
this.delegationUsersByService;
+
+        if (delegationUsersByService.isEmpty()) {
+            ret = false;
+        } else if (StringUtils.isBlank(serviceName) || 
StringUtils.isBlank(userName)) {
+            ret = false;
+        } else {
+            Set<String> delegationUsers = 
delegationUsersByService.get(serviceName);
+
+            if (delegationUsers == null) {
+                delegationUsers = 
delegationUsersByService.get(RangerPdpConstants.WILDCARD_SERVICE_NAME);
+            }
+
+            ret = delegationUsers != null && 
delegationUsers.contains(userName);
+        }
+
+        LOG.debug("isDelegationUserForService(serviceName={}, userName={}): 
ret={}", serviceName, userName, ret);
+
+        return ret;
+    }
+
+    private void initializeDelegationUsers() {
+        Properties properties = config != null ? config.getAuthzProperties() : 
new Properties();
+
+        for (String key : properties.stringPropertyNames()) {
+            if (key.startsWith(PROP_PDP_SERVICE_PREFIX) && 
key.endsWith(PROP_SUFFIX_DELEGATION_USERS)) {
+                String serviceName = 
key.substring(PROP_PDP_SERVICE_PREFIX.length(), key.length() - 
PROP_SUFFIX_DELEGATION_USERS.length());
+
+                if (StringUtils.isBlank(serviceName)) {
+                    continue;
+                }
+
+                Set<String> delegationUsers = 
Arrays.stream(StringUtils.defaultString(properties.getProperty(key)).split(","))
+                                                 .map(String::trim)
+                                                 
.filter(StringUtils::isNotBlank)
+                                                 .collect(Collectors.toSet());
+
+                if (!delegationUsers.isEmpty()) {
+                    delegationUsersByService.put(serviceName, delegationUsers);
+
+                    LOG.info("Delegation users for service '{}': {}", 
serviceName, delegationUsers);
+                }
+            }
+        }
+
+        if (delegationUsersByService.isEmpty()) {
+            LOG.warn("No delegation users configured");
+        } else {
+            Set<String> wildcardDelegationUsers = 
delegationUsersByService.get(WILDCARD_SERVICE_NAME);
+
+            // delegation users for WILDCARD_SERVICE_NAME are delegates for 
all services
+            if (wildcardDelegationUsers != null && 
!wildcardDelegationUsers.isEmpty()) {
+                delegationUsersByService.forEach((k, v) -> 
v.addAll(wildcardDelegationUsers));
+            }
+        }
+    }
+
+    private Response badRequest(RangerAuthzException e) {
+        return Response.status(Response.Status.BAD_REQUEST)
+                       .entity(new ErrorResponse(Response.Status.BAD_REQUEST, 
e.getMessage()))
+                       .build();
+    }
+
+    private Response serverError(Exception e) {
+        return Response.serverError()
+                       .entity(new 
ErrorResponse(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage()))
+                       .build();

Review Comment:
   `serverError()` returns `e.getMessage()` to the client. For unexpected 500s 
this can leak internal details (class names, paths, config, etc.) and is also 
unstable as an API surface. Consider returning a generic message (optionally 
including a requestId) and keep the full exception details only in server logs.



##########
pdp/conf.dist/ranger-pdp-site.xml:
##########
@@ -0,0 +1,365 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!-- Copy this file to conf/ranger-pdp-site.xml and edit to override defaults. 
-->
+<configuration>
+  <property>
+    <name>ranger.pdp.port</name>
+    <value>6500</value>
+    <description>Port the PDP server listens on.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.log.dir</name>
+    <value>/var/log/ranger/pdp</value>
+    <description>Directory for PDP server log files.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.*.delegation.users</name>
+    <value/>
+    <description>Comma-separated users, allowed to call on behalf of other 
users in all services</description>
+  </property>
+
+  <!-- SSL/TLS -->
+  <property>
+    <name>ranger.pdp.ssl.enabled</name>
+    <value>false</value>
+    <description>Set to true to enable HTTPS.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.file</name>
+    <value/>
+    <description>Path to the keystore file (required when SSL is 
enabled).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.password</name>
+    <value/>
+    <description>Keystore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.type</name>
+    <value>JKS</value>
+    <description>Keystore type (JKS, PKCS12).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.enabled</name>
+    <value>false</value>
+    <description>Set to true to require client certificate 
authentication.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.file</name>
+    <value/>
+    <description>Path to the truststore file.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.password</name>
+    <value/>
+    <description>Truststore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.type</name>
+    <value>JKS</value>
+    <description>Truststore type (JKS, PKCS12).</description>
+  </property>
+
+  <!-- HTTP/2 -->
+  <property>
+    <name>ranger.pdp.http2.enabled</name>
+    <value>true</value>
+    <description>
+      Enable HTTP/2 via upgrade on the connector.
+      Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside 
HTTP/1.1.
+    </description>
+  </property>
+
+  <!-- Connector concurrency / queue limits -->
+  <property>
+    <name>ranger.pdp.http.connector.maxThreads</name>
+    <value>200</value>
+    <description>Maximum number of worker threads handling simultaneous 
requests.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.minSpareThreads</name>
+    <value>20</value>
+    <description>Minimum number of spare worker threads kept 
ready.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.acceptCount</name>
+    <value>100</value>
+    <description>Queued connection backlog when all worker threads are 
busy.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.maxConnections</name>
+    <value>10000</value>
+    <description>Maximum concurrent TCP connections accepted by the 
connector.</description>
+  </property>
+
+  <!-- Authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.auth.types</name>
+    <value>jwt,kerberos</value>
+    <description>
+      Comma-separated list of authentication methods for incoming REST 
requests,
+      tried in listed order. Supported values: header, jwt, kerberos.

Review Comment:
   Same as `ranger-pdp-default.xml`: `ranger.pdp.auth.types` defaults to 
`jwt,kerberos` while Kerberos principal/keytab are empty in this template. 
Unless Kerberos is always configured, this default will cause startup failures. 
Consider changing the template default to a working combination (e.g., `jwt`) 
or adding a prominent note next to the property.
   ```suggestion
       <value>jwt</value>
       <description>
         Comma-separated list of authentication methods for incoming REST 
requests,
         tried in listed order. Supported values: header, jwt, kerberos.
         Add 'kerberos' here only after configuring the required Kerberos 
principal and keytab.
   ```



##########
pdp/scripts/ranger-pdp-services.sh:
##########
@@ -0,0 +1,223 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+if [[ -z $1 ]]; then
+    echo "No argument provided."
+    echo "Usage: $0 {start | run | stop | restart | version}"
+    exit 1
+fi
+
+action=$1
+action=$(echo "$action" | tr '[:lower:]' '[:upper:]')
+
+realScriptPath=$(readlink -f "$0")
+realScriptDir=$(dirname "$realScriptPath")
+cd "$realScriptDir" || exit 1
+cdir=$(pwd)
+
+ranger_pdp_max_heap_size=${RANGER_PDP_MAX_HEAP_SIZE:-1g}
+
+for custom_env_script in $(find "${cdir}/conf/" -name "ranger-pdp-env*" 
2>/dev/null); do
+    if [ -f "$custom_env_script" ]; then
+        . "$custom_env_script"
+    fi
+done
+
+if [ -z "${RANGER_PDP_PID_DIR_PATH}" ]; then
+    RANGER_PDP_PID_DIR_PATH=/var/run/ranger
+fi
+
+if [ -z "${RANGER_PDP_PID_NAME}" ]; then
+    RANGER_PDP_PID_NAME=pdp.pid
+fi
+
+pidf="${RANGER_PDP_PID_DIR_PATH}/${RANGER_PDP_PID_NAME}"
+
+if [ -z "${UNIX_PDP_USER}" ]; then
+    UNIX_PDP_USER=ranger
+fi
+
+JAVA_OPTS=" ${JAVA_OPTS} -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=200m 
-Xmx${ranger_pdp_max_heap_size} -Xms256m"
+
+if [ "${action}" == "START" ]; then
+
+    if [ -f "${cdir}/conf/java_home.sh" ]; then
+        . "${cdir}/conf/java_home.sh"
+    fi
+
+    for custom_env_script in $(find "${cdir}/conf.dist/" -name 
"ranger-pdp-env*" 2>/dev/null); do
+        if [ -f "$custom_env_script" ]; then
+            . "$custom_env_script"
+        fi
+    done
+
+    if [ "$JAVA_HOME" != "" ]; then
+        export PATH=$JAVA_HOME/bin:$PATH
+    fi
+
+    if [ -z "${RANGER_PDP_LOG_DIR}" ]; then
+        RANGER_PDP_LOG_DIR=/var/log/ranger/pdp
+    fi
+
+    if [ ! -d "$RANGER_PDP_LOG_DIR" ]; then
+        mkdir -p "$RANGER_PDP_LOG_DIR"
+        chmod 777 "$RANGER_PDP_LOG_DIR"

Review Comment:
   Creating the log directory with `chmod 777` makes it world-writable, which 
is risky on multi-tenant hosts. Consider using a more restrictive permission 
(e.g., 750/755) and ensuring ownership is set appropriately instead of granting 
write access to all users.
   ```suggestion
           if [ -n "${UNIX_PDP_USER}" ]; then
               chown "${UNIX_PDP_USER}":"${UNIX_PDP_USER}" 
"$RANGER_PDP_LOG_DIR" 2>/dev/null || true
               chmod 750 "$RANGER_PDP_LOG_DIR"
           else
               chmod 755 "$RANGER_PDP_LOG_DIR"
           fi
   ```



##########
pdp/src/main/resources/ranger-pdp-default.xml:
##########
@@ -0,0 +1,364 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<configuration>
+  <property>
+    <name>ranger.pdp.port</name>
+    <value>6500</value>
+    <description>Port the PDP server listens on.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.log.dir</name>
+    <value>/var/log/ranger/pdp</value>
+    <description>Directory for PDP server log files.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.service.*.delegation.users</name>
+    <value/>
+    <description>Comma-separated users, allowed to call on behalf of other 
users in all services</description>
+  </property>
+
+  <!-- SSL/TLS -->
+  <property>
+    <name>ranger.pdp.ssl.enabled</name>
+    <value>false</value>
+    <description>Set to true to enable HTTPS.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.file</name>
+    <value/>
+    <description>Path to the keystore file (required when SSL is 
enabled).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.password</name>
+    <value/>
+    <description>Keystore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.keystore.type</name>
+    <value>JKS</value>
+    <description>Keystore type (JKS, PKCS12).</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.enabled</name>
+    <value>false</value>
+    <description>Set to true to require client certificate 
authentication.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.file</name>
+    <value/>
+    <description>Path to the truststore file.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.password</name>
+    <value/>
+    <description>Truststore password.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.ssl.truststore.type</name>
+    <value>JKS</value>
+    <description>Truststore type (JKS, PKCS12).</description>
+  </property>
+
+  <!-- HTTP/2 -->
+  <property>
+    <name>ranger.pdp.http2.enabled</name>
+    <value>true</value>
+    <description>
+      Enable HTTP/2 via upgrade on the connector.
+      Supports both h2 (over TLS) and h2c (cleartext upgrade) alongside 
HTTP/1.1.
+    </description>
+  </property>
+
+  <!-- Connector concurrency / queue limits -->
+  <property>
+    <name>ranger.pdp.http.connector.maxThreads</name>
+    <value>200</value>
+    <description>Maximum number of worker threads handling simultaneous 
requests.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.minSpareThreads</name>
+    <value>20</value>
+    <description>Minimum number of spare worker threads kept 
ready.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.acceptCount</name>
+    <value>100</value>
+    <description>Queued connection backlog when all worker threads are 
busy.</description>
+  </property>
+
+  <property>
+    <name>ranger.pdp.http.connector.maxConnections</name>
+    <value>10000</value>
+    <description>Maximum concurrent TCP connections accepted by the 
connector.</description>
+  </property>
+
+  <!-- Authentication for incoming REST requests -->
+  <property>
+    <name>ranger.pdp.auth.types</name>
+    <value>jwt,kerberos</value>
+    <description>
+      Comma-separated list of authentication methods for incoming REST 
requests,
+      tried in listed order. Supported values: header, jwt, kerberos.
+    </description>
+  </property>

Review Comment:
   The default `ranger.pdp.auth.types` includes `kerberos`, but the default 
Kerberos principal/keytab values are empty. With the current auth filter 
behavior this will make the server fail to start unless Kerberos is configured. 
Consider defaulting this to `jwt` (or `jwt,header`), or document clearly that 
Kerberos must be removed unless configured.



##########
pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java:
##########
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.pdp.config;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * Reads Ranger PDP configuration from {@code ranger-pdp-default.xml} 
(classpath)
+ * overridden by {@code ranger-pdp-site.xml} (classpath or filesystem).
+ *
+ * <p>Both files use the Hadoop {@code <configuration>} XML format, consistent
+ * with other Ranger server modules (tagsync, kms, etc.).
+ * The format is parsed directly using the JDK DOM API to avoid an early
+ * class-load dependency on Hadoop's {@code Configuration} class.
+ *
+ * <p>Authentication property names:
+ * <ul>
+ *   <li>Kerberos/SPNEGO:    {@code ranger.pdp.kerberos.spnego.*}
+ *   <li>JWT bearer token:   {@code ranger.pdp.jwt.*}
+ *   <li>HTTP header:        {@code ranger.pdp.authn.header.*}
+ * </ul>
+ */
+public class RangerPdpConfig {
+    private static final Logger LOG = 
LoggerFactory.getLogger(RangerPdpConfig.class);
+
+    private static final String DEFAULT_CONFIG_FILE = "ranger-pdp-default.xml";
+    private static final String SITE_CONFIG_FILE    = "ranger-pdp-site.xml";
+
+    private final Properties props;
+
+    public RangerPdpConfig() {
+        props = new Properties();
+
+        loadFromClasspath(DEFAULT_CONFIG_FILE);
+        loadFromClasspath(SITE_CONFIG_FILE);
+
+        String confDir = System.getProperty(RangerPdpConstants.PROP_CONF_DIR, 
"");
+
+        if (StringUtils.isNotBlank(confDir)) {
+            loadFromFile(new File(confDir, SITE_CONFIG_FILE));
+        }
+
+        applySystemPropertyOverrides();
+
+        LOG.info("RangerPdpConfig initialized (conf.dir={})", confDir);
+    }
+
+    public int getPort() {
+        return getInt(RangerPdpConstants.PROP_PORT, 6500);
+    }
+
+    public String getLogDir() {
+        return get(RangerPdpConstants.PROP_LOG_DIR, "/var/log/ranger/pdp");
+    }
+
+    public boolean isSslEnabled() {
+        return getBoolean(RangerPdpConstants.PROP_SSL_ENABLED, false);
+    }
+
+    public String getKeystoreFile() {
+        return get(RangerPdpConstants.PROP_SSL_KEYSTORE_FILE, "");
+    }
+
+    public String getKeystorePassword() {
+        return get(RangerPdpConstants.PROP_SSL_KEYSTORE_PASSWORD, "");
+    }
+
+    public String getKeystoreType() {
+        return get(RangerPdpConstants.PROP_SSL_KEYSTORE_TYPE, "JKS");
+    }
+
+    public boolean isTruststoreEnabled() {
+        return getBoolean(RangerPdpConstants.PROP_SSL_TRUSTSTORE_ENABLED, 
false);
+    }
+
+    public String getTruststoreFile() {
+        return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_FILE, "");
+    }
+
+    public String getTruststorePassword() {
+        return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_PASSWORD, "");
+    }
+
+    public String getTruststoreType() {
+        return get(RangerPdpConstants.PROP_SSL_TRUSTSTORE_TYPE, "JKS");
+    }
+
+    public boolean isHttp2Enabled() {
+        return getBoolean(RangerPdpConstants.PROP_HTTP2_ENABLED, true);
+    }
+
+    public int getHttpConnectorMaxThreads() {
+        return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_THREADS, 200);
+    }
+
+    public int getHttpConnectorMinSpareThreads() {
+        return 
getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MIN_SPARE_THREADS, 20);
+    }
+
+    public int getHttpConnectorAcceptCount() {
+        return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_ACCEPT_COUNT, 
100);
+    }
+
+    public int getHttpConnectorMaxConnections() {
+        return getInt(RangerPdpConstants.PROP_HTTP_CONNECTOR_MAX_CONNECTIONS, 
10000);
+    }
+
+    public String getAuthTypes() {
+        return get(RangerPdpConstants.PROP_AUTH_TYPES, "jwt,kerberos");
+    }
+
+    // --- HTTP Header auth ---
+
+    public boolean isHeaderAuthnEnabled() {
+        return getBoolean(RangerPdpConstants.PROP_HEADER_AUTHN_ENABLED, false);
+    }
+
+    public String getHeaderAuthnUsername() {
+        return get(RangerPdpConstants.PROP_HEADER_AUTHN_USERNAME, 
"X-Forwarded-User");
+    }
+
+    // --- JWT bearer token auth ---
+
+    public String getJwtProviderUrl() {
+        return get(RangerPdpConstants.PROP_JWT_PROVIDER_URL, "");
+    }
+
+    public String getJwtPublicKey() {
+        return get(RangerPdpConstants.PROP_JWT_PUBLIC_KEY, "");
+    }
+
+    public String getJwtCookieName() {
+        return get(RangerPdpConstants.PROP_JWT_COOKIE_NAME, "hadoop-jwt");
+    }
+
+    public String getJwtAudiences() {
+        return get(RangerPdpConstants.PROP_JWT_AUDIENCES, "");
+    }
+
+    // --- Kerberos / SPNEGO ---
+
+    public String getSpnegoPrincipal() {
+        return get(RangerPdpConstants.PROP_SPNEGO_PRINCIPAL, "");
+    }
+
+    public String getSpnegoKeytab() {
+        return get(RangerPdpConstants.PROP_SPNEGO_KEYTAB, "");
+    }
+
+    public String getKerberosNameRules() {
+        return get(RangerPdpConstants.PROP_KRB_NAME_RULES, "DEFAULT");
+    }
+
+    public int getKerberosTokenValiditySeconds() {
+        return getInt(RangerPdpConstants.PROP_KRB_TOKEN_VALIDITY, 30);
+    }
+
+    public String getKerberosCookieDomain() {
+        return get(RangerPdpConstants.PROP_KRB_COOKIE_DOMAIN, "");
+    }
+
+    public String getKerberosCookiePath() {
+        return get(RangerPdpConstants.PROP_KRB_COOKIE_PATH, "/");
+    }
+
+    /**
+     * Returns all properties for forwarding to {@code 
RangerEmbeddedAuthorizer}.
+     */
+    public Properties getAuthzProperties() {
+        return new Properties(props);
+    }
+
+    public String get(String key, String defaultValue) {
+        String val = props.getProperty(key);
+
+        return StringUtils.isNotBlank(val) ? val.trim() : defaultValue;
+    }
+
+    public int getInt(String key, int defaultValue) {
+        String val = props.getProperty(key);
+
+        if (StringUtils.isNotBlank(val)) {
+            try {
+                return Integer.parseInt(val.trim());
+            } catch (NumberFormatException e) {
+                LOG.warn("Invalid integer for {}: '{}'; using default {}", 
key, val, defaultValue);
+            }
+        }
+
+        return defaultValue;
+    }
+
+    public boolean getBoolean(String key, boolean defaultValue) {
+        String val = props.getProperty(key);
+
+        return StringUtils.isNotBlank(val) ? Boolean.parseBoolean(val.trim()) 
: defaultValue;
+    }
+
+    private void loadFromClasspath(String resourceName) {
+        try (InputStream in = 
getClass().getClassLoader().getResourceAsStream(resourceName)) {
+            if (in != null) {
+                parseHadoopXml(in, resourceName);
+            } else {
+                LOG.debug("Config resource not found on classpath: {}", 
resourceName);
+            }
+        } catch (IOException e) {
+            LOG.warn("Failed to close stream for classpath resource: {}", 
resourceName, e);
+        }
+    }
+
+    private void loadFromFile(File file) {
+        if (!file.exists() || !file.isFile()) {
+            LOG.debug("Config file not found: {}", file);
+            return;
+        }
+
+        try (InputStream in = new FileInputStream(file)) {
+            parseHadoopXml(in, file.getAbsolutePath());
+        } catch (IOException e) {
+            LOG.warn("Failed to read config file: {}", file, e);
+        }
+    }
+
+    /**
+     * Parses a Hadoop-style {@code <configuration>} XML document and merges 
all
+     * {@code <property>} entries into {@link #props}.  Later entries override 
earlier
+     * ones, matching Hadoop's own override semantics.
+     *
+     * <pre>
+     * {@code
+     * <configuration>
+     *   <property>
+     *     <name>some.key</name>
+     *     <value>some-value</value>
+     *   </property>
+     * </configuration>
+     * }
+     * </pre>
+     */
+    private void parseHadoopXml(InputStream in, String source) {
+        try {
+            DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+
+            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+            
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl";, 
true);
+
+            DocumentBuilder builder = factory.newDocumentBuilder();

Review Comment:
   The XML parsing hardening here only disallows DOCTYPE; unlike other XML 
parsers in this repo it doesn't explicitly disable external general/parameter 
entities. To reduce XXE risk if a config file is ever attacker-controlled, set 
the standard JAXP features 
(external-general-entities/external-parameter-entities to false) and disable 
XInclude/expandEntityReferences, consistent with `common-utils/.../XMLUtils`.



##########
pdp/src/main/java/org/apache/ranger/pdp/security/JwtAuthHandler.java:
##########
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.pdp.security;
+
+import org.apache.ranger.authz.handler.RangerAuth;
+import org.apache.ranger.authz.handler.jwt.RangerDefaultJwtAuthHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Authenticates requests using a JWT bearer token.
+ *
+ * <p>Checks for the token in the {@code Authorization: Bearer <token>} header 
first,
+ * then in the configured JWT cookie.  Delegates signature verification and 
expiry/audience
+ * checks to {@link RangerDefaultJwtAuthHandler} from the {@code ranger-authn} 
module.
+ *
+ * <p>Configuration keys (all prefixed with {@code ranger.pdp.auth.jwt.}):
+ * <ul>
+ *   <li>{@code provider.url}  – JWKS endpoint URL (optional if public key is 
set)
+ *   <li>{@code public.key}    – PEM-encoded public key (optional if provider 
URL is set)
+ *   <li>{@code cookie.name}   – JWT cookie name (default: {@code hadoop-jwt})
+ *   <li>{@code audiences}     – comma-separated list of accepted audiences 
(optional)
+ * </ul>

Review Comment:
   The Javadoc claims JWT config keys are prefixed with `ranger.pdp.auth.jwt.` 
but the actual config keys used/forwarded are `ranger.pdp.jwt.*` (see 
`RangerPdpConstants.PROP_JWT_*`). Please correct the prefix in the 
documentation to match the real configuration keys.



##########
dev-support/ranger-docker/Dockerfile.ranger-pdp:
##########
@@ -0,0 +1,41 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+ARG RANGER_BASE_IMAGE=apache/ranger-base
+ARG RANGER_BASE_VERSION=20260123-2-8
+
+FROM ${RANGER_BASE_IMAGE}:${RANGER_BASE_VERSION}
+
+ARG PDP_VERSION
+
+COPY ./dist/ranger-${PDP_VERSION}-pdp.tar.gz /home/ranger/dist/
+COPY ./scripts/pdp/ranger-pdp.sh             ${RANGER_SCRIPTS}/
+
+RUN    tar xvfz /home/ranger/dist/ranger-${PDP_VERSION}-pdp.tar.gz 
--directory=${RANGER_HOME} \
+    && ln -s ${RANGER_HOME}/ranger-${PDP_VERSION}-pdp ${RANGER_HOME}/pdp \
+    && rm -f /home/ranger/dist/ranger-${PDP_VERSION}-pdp.tar.gz \
+    && mkdir -p /var/log/ranger/pdp /etc/ranger/cache /etc/init.d /etc/rc2.d 
/etc/rc3.d \
+    && touch /etc/init.d/ranger-pdp \
+    && ln -s /etc/init.d/ranger-pdp /etc/rc2.d/S99ranger-pdp \
+    && ln -s /etc/init.d/ranger-pdp /etc/rc2.d/K00ranger-pdp \
+    && ln -s /etc/init.d/ranger-pdp /etc/rc3.d/S99ranger-pdp \
+    && ln -s /etc/init.d/ranger-pdp /etc/rc3.d/K00ranger-pdp \

Review Comment:
   The Dockerfile creates an empty `/etc/init.d/ranger-pdp` and then adds 
runlevel symlinks to it, but doesn't populate it with the actual init wrapper 
script (`ranger-pdp`). This can be confusing and may break environments that 
rely on init scripts. Consider copying the real init script into 
`/etc/init.d/ranger-pdp` (or dropping the runlevel links entirely if not needed 
in the container).
   ```suggestion
   
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to