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


##########
intg/src/main/python/apache_ranger/exceptions.py:
##########
@@ -38,9 +38,15 @@ def __init__(self, api, response):
         if api is not None and response is not None:
             if response.content:
               try:
-                respJson         = response.json()
-                self.msgDesc     = respJson['msgDesc']     if respJson is not 
None and 'msgDesc'     in respJson else None
-                self.messageList = respJson['messageList'] if respJson is not 
None and 'messageList' in respJson else None
+                respJson = response.json()

Review Comment:
   `print(response)` in the exception constructor will write to stdout on every 
API error, which is noisy for library consumers and can leak response details 
into logs/terminals unexpectedly. Consider removing it or switching to 
`LOG.debug(...)`/`LOG.exception(...)` behind a logger.



##########
pdp/src/main/java/org/apache/ranger/pdp/RangerPdpServer.java:
##########
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.pdp;
+
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.valves.AccessLogValve;
+import org.apache.coyote.http2.Http2Protocol;
+import org.apache.ranger.authz.api.RangerAuthorizer;
+import org.apache.ranger.authz.api.RangerAuthzException;
+import org.apache.ranger.authz.embedded.RangerEmbeddedAuthorizer;
+import org.apache.ranger.pdp.config.RangerPdpConfig;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.apache.ranger.pdp.rest.RangerPdpApplication;
+import org.apache.ranger.pdp.security.RangerPdpAuthFilter;
+import org.apache.ranger.pdp.security.RangerPdpRequestContextFilter;
+import org.apache.tomcat.util.descriptor.web.FilterDef;
+import org.apache.tomcat.util.descriptor.web.FilterMap;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServlet;
+
+import java.io.File;
+
+/**
+ * Main entry point for the Ranger Policy Decision Point (PDP) server.
+ *
+ * <p>Starts an embedded Apache Tomcat instance that:
+ * <ul>
+ *   <li>Creates and initialises a {@link RangerEmbeddedAuthorizer} singleton
+ *   <li>Exposes the three authorizer methods as REST endpoints under {@code 
/authz/v1/}
+ *   <li>Enforces authentication via {@link RangerPdpAuthFilter} 
(Kerberos/JWT/HTTP-Header)
+ *   <li>Optionally enables HTTP/2 ({@code Http2Protocol} upgrade on the 
connector)
+ * </ul>
+ *
+ * <p><b>Startup:</b> {@code java -jar ranger-pdp.jar}
+ * <br>Override config with: {@code -Dranger.pdp.conf.dir=/etc/ranger/pdp}
+ */
+public class RangerPdpServer {
+    private static final Logger LOG = 
LoggerFactory.getLogger(RangerPdpServer.class);
+
+    private final RangerPdpConfig  config;
+    private final RangerPdpStats   runtimeStats = new RangerPdpStats();
+    private       Tomcat           tomcat;
+    private       RangerAuthorizer authorizer;
+
+    public RangerPdpServer() {
+        this.config = new RangerPdpConfig();
+    }
+
+    public static void main(String[] args) throws Exception {
+        new RangerPdpServer().start();
+    }
+
+    public void start() throws Exception {
+        LOG.info("Starting Ranger PDP server");
+
+        initAuthorizer();
+        startTomcat();
+    }
+
+    public void stop() {
+        LOG.info("Stopping Ranger PDP server");
+
+        runtimeStats.setAcceptingRequests(false);
+        runtimeStats.setServerStarted(false);
+
+        if (tomcat != null) {
+            try {
+                if (tomcat.getConnector() != null) {
+                    tomcat.getConnector().pause();
+                }
+
+                tomcat.stop();
+                tomcat.destroy();
+            } catch (LifecycleException e) {
+                LOG.warn("Error stopping Tomcat", e);
+            }
+        }
+
+        if (authorizer != null) {
+            try {
+                authorizer.close();
+            } catch (RangerAuthzException e) {
+                LOG.warn("Error closing authorizer", e);
+            }
+        }
+    }
+
+    private void initAuthorizer() throws RangerAuthzException {
+        authorizer = new RangerEmbeddedAuthorizer(config.getAuthzProperties());
+
+        authorizer.init();
+
+        runtimeStats.setAuthorizerInitialized(true);
+
+        LOG.info("RangerEmbeddedAuthorizer initialised");
+    }
+
+    private void startTomcat() throws Exception {
+        tomcat = new Tomcat();
+
+        tomcat.setConnector(createConnector());
+
+        String docBase = new File(System.getProperty("java.io.tmpdir"), 
"ranger-pdp-webapps").getAbsolutePath();
+
+        new File(docBase).mkdirs();
+
+        Context ctx = tomcat.addContext("", docBase);
+
+        
ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_AUTHORIZER,
 authorizer);
+        
ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_CONFIG,
 config);
+        
ctx.getServletContext().setAttribute(RangerPdpConstants.SERVLET_CTX_ATTR_RUNTIME_STATE,
 runtimeStats);
+
+        addAuthFilter(ctx);
+        addJerseyServlet(ctx);
+        addStatusEndpoints(ctx);
+        addAccessLogValve();
+
+        Runtime.getRuntime().addShutdownHook(new Thread(this::stop, 
"ranger-pdp-shutdown"));
+
+        tomcat.start();
+
+        runtimeStats.setServerStarted(true);
+        runtimeStats.setAcceptingRequests(true);
+
+        LOG.info("Ranger PDP server listening on port {} (SSL={}, HTTP/2={})", 
config.getPort(), config.isSslEnabled(), config.isHttp2Enabled());
+
+        tomcat.getServer().await();
+    }
+
+    /**
+     * Builds the Tomcat connector.
+     *
+     * <p>When SSL is enabled the connector is configured as an HTTPS endpoint.
+     * When HTTP/2 is enabled an {@link Http2Protocol} upgrade protocol is 
added,
+     * supporting both {@code h2} (over TLS) and {@code h2c} (cleartext 
upgrade) depending
+     * on whether SSL is enabled.
+     */
+    private Connector createConnector() {
+        Connector connector = new 
Connector("org.apache.coyote.http11.Http11NioProtocol");
+
+        connector.setPort(config.getPort());
+        connector.setProperty("maxThreads", 
String.valueOf(config.getHttpConnectorMaxThreads()));
+        connector.setProperty("minSpareThreads", 
String.valueOf(config.getHttpConnectorMinSpareThreads()));
+        connector.setProperty("acceptCount", 
String.valueOf(config.getHttpConnectorAcceptCount()));
+        connector.setProperty("maxConnections", 
String.valueOf(config.getHttpConnectorMaxConnections()));
+
+        LOG.info("Configured HTTP connector limits: maxThreads={}, 
minSpareThreads={}, acceptCount={}, maxConnections={}",
+                config.getHttpConnectorMaxThreads(), 
config.getHttpConnectorMinSpareThreads(),
+                config.getHttpConnectorAcceptCount(), 
config.getHttpConnectorMaxConnections());
+
+        if (config.isSslEnabled()) {
+            connector.setSecure(true);
+            connector.setScheme("https");
+            connector.setProperty("SSLEnabled", "true");
+            connector.setProperty("protocol", 
"org.apache.coyote.http11.Http11NioProtocol");
+            connector.setProperty("keystoreFile", config.getKeystoreFile());
+            connector.setProperty("keystorePass", 
config.getKeystorePassword());
+            connector.setProperty("keystoreType", config.getKeystoreType());
+            connector.setProperty("sslProtocol", "TLS");
+
+            if (config.isTruststoreEnabled()) {
+                connector.setProperty("truststoreFile",  
config.getTruststoreFile());
+                connector.setProperty("truststorePass",  
config.getTruststorePassword());
+                connector.setProperty("truststoreType",  
config.getTruststoreType());
+                connector.setProperty("clientAuth",      "want");
+            }
+        }
+
+        if (config.isHttp2Enabled()) {
+            connector.addUpgradeProtocol(new Http2Protocol());
+
+            LOG.info("HTTP/2 upgrade protocol registered on connector 
(port={})", config.getPort());
+        }
+
+        return connector;
+    }
+
+    /**
+     * Registers {@link RangerPdpAuthFilter} on all {@code /authz/*} paths.
+     * Init parameters are forwarded from the server config so the filter can
+     * instantiate and configure the auth handlers.
+     */
+    private void addAuthFilter(Context ctx) {
+        FilterDef reqCtxFilterDef = new FilterDef();
+        FilterMap reqCtxFilterMap = new FilterMap();
+        FilterDef authFilterDef = new FilterDef();
+        FilterMap authFilterMap = new FilterMap();
+
+        reqCtxFilterDef.setFilterName("rangerPdpRequestContextFilter");
+        reqCtxFilterDef.setFilter(new RangerPdpRequestContextFilter());
+
+        reqCtxFilterMap.setFilterName("rangerPdpRequestContextFilter");
+        reqCtxFilterMap.addURLPattern("/*");
+
+        authFilterDef.setFilterName("rangerPdpAuthFilter");
+        authFilterDef.setFilter(new RangerPdpAuthFilter());
+        authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_AUTH_TYPES,   
        config.getAuthTypes());
+        // HTTP Header auth
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_ENABLED,  
String.valueOf(config.isHeaderAuthnEnabled()));
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_HEADER_AUTHN_USERNAME, 
config.getHeaderAuthnUsername());
+        // JWT bearer token auth
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_PROVIDER_URL, 
config.getJwtProviderUrl());
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_PUBLIC_KEY,   
config.getJwtPublicKey());
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_COOKIE_NAME,  
config.getJwtCookieName());
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_JWT_AUDIENCES,    
config.getJwtAudiences());
+        // Kerberos / SPNEGO
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_SPNEGO_PRINCIPAL,     
config.getSpnegoPrincipal());
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_SPNEGO_KEYTAB,        
config.getSpnegoKeytab());
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_NAME_RULES,       
config.getKerberosNameRules());
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_TOKEN_VALIDITY,   
String.valueOf(config.getKerberosTokenValiditySeconds()));
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_COOKIE_DOMAIN,    
config.getKerberosCookieDomain());
+        
authFilterDef.addInitParameter(RangerPdpAuthFilter.PARAM_KRB_COOKIE_PATH,      
config.getKerberosCookiePath());

Review Comment:
   The filter init parameters `ranger.pdp.authn.kerberos.token.valid.seconds`, 
`cookie.domain`, and `cookie.path` are forwarded here, but the current 
`KerberosAuthHandler` implementation doesn't read/use them. Consider either 
implementing the intended behavior (cookie/token handling) or removing these 
params from the filter wiring/config to avoid misleading configuration knobs.
   ```suggestion
   
   ```



##########
pdp/src/main/java/org/apache/ranger/pdp/security/RangerPdpAuthFilter.java:
##########
@@ -0,0 +1,199 @@
+/*
+ * 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_AUTHN_HEADER_ENABLED;
+    public static final String PARAM_HEADER_AUTHN_USERNAME = 
RangerPdpConstants.PROP_AUTHN_HEADER_USERNAME;
+
+    // JWT bearer token auth
+    public static final String PARAM_JWT_PROVIDER_URL = 
RangerPdpConstants.PROP_AUTHN_JWT_PROVIDER_URL;
+    public static final String PARAM_JWT_PUBLIC_KEY   = 
RangerPdpConstants.PROP_AUTHN_JWT_PUBLIC_KEY;
+    public static final String PARAM_JWT_COOKIE_NAME  = 
RangerPdpConstants.PROP_AUTHN_JWT_COOKIE_NAME;
+    public static final String PARAM_JWT_AUDIENCES    = 
RangerPdpConstants.PROP_AUTHN_JWT_AUDIENCES;
+
+    // Kerberos / SPNEGO
+    public static final String PARAM_SPNEGO_PRINCIPAL   = 
RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL;
+    public static final String PARAM_SPNEGO_KEYTAB      = 
RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB;
+    public static final String PARAM_KRB_NAME_RULES     = 
RangerPdpConstants.PROP_KRB_NAME_RULES;
+    public static final String PARAM_KRB_TOKEN_VALIDITY = 
RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_TOKEN_VALIDITY;
+    public static final String PARAM_KRB_COOKIE_DOMAIN  = 
RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_DOMAIN;
+    public static final String PARAM_KRB_COOKIE_PATH    = 
RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_PATH;
+
+    private final List<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 = "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) {
+                LOG.warn("Unknown auth type ignored: {}", type);
+
+                continue;
+            }
+
+            try {
+                handler.init(config);
+                handlers.add(handler);
+
+                LOG.info("Registered auth handler: {}", normalizedType);
+            } catch (Exception e) {
+                LOG.error("Failed to initialize auth handler: {}", type, e);
+            }

Review Comment:
   `init()` logs and continues when an explicitly configured auth handler fails 
to initialize. This can result in the server starting with a reduced (and 
potentially unintended) authentication surface (e.g., `jwt,kerberos` configured 
but Kerberos misconfigured => JWT-only). Consider failing filter initialization 
when any configured handler cannot be initialized (or at least when a handler 
listed in `ranger.pdp.auth.types` fails), so misconfiguration doesn't silently 
weaken auth.



##########
pdp/conf.dist/logback.xml:
##########
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<configuration>
+  <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
+    <Target>System.out</Target>
+    <encoder>
+      <pattern>%d{ISO8601} %-5p [%X{requestId}] %c{1} - %m%n</pattern>
+    </encoder>
+  </appender>
+  <logger 
name="com.sun.jersey.server.wadl.generators.WadlGeneratorJAXBGrammarGenerator" 
level="OFF"/>
+  <logger name="org.apache.directory.server.core" level="OFF"/>
+  <logger name="org.apache.hadoop.security" level="OFF"/>
+  <logger name="org.apache.hadoop.conf" level="ERROR"/>
+  <logger name="org.apache.hadoop.crytpo.key.kms.server" level="ALL"/>

Review Comment:
   Logger name `org.apache.hadoop.crytpo.key.kms.server` looks like a typo 
(expected `org.apache.hadoop.crypto.key.kms.server`). As written, this logger 
configuration won’t match the actual Hadoop KMS classes, so the intended 
logging level change won’t take effect.
   ```suggestion
     <logger name="org.apache.hadoop.crypto.key.kms.server" level="ALL"/>
   ```



##########
intg/src/main/python/apache_ranger/model/ranger_authz.py:
##########
@@ -0,0 +1,259 @@
+#!/usr/bin/env python
+
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from apache_ranger.model.ranger_base import RangerBase
+from apache_ranger.utils             import non_null, type_coerce, 
type_coerce_dict, type_coerce_list
+
+
+class RangerUserInfo(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.name       = attrs.get("name")
+        self.attributes = attrs.get("attributes")
+        self.groups     = attrs.get("groups")
+        self.roles      = attrs.get("roles")
+
+
+class RangerResourceInfo(RangerBase):
+    SCOPE_SELF                   = "SELF"
+    SCOPE_SELF_OR_ANY_CHILD      = "SELF_OR_ANY_CHILD"
+    SCOPE_SELF_OR_ANY_DESCENDANT = "SELF_OR_ANY_DESCENDANT"
+
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.name           = attrs.get("name")
+        self.subResources   = attrs.get("subResources")
+        self.nameMatchScope = attrs.get("nameMatchScope")
+        self.attributes     = attrs.get("attributes")
+
+
+class RangerAccessInfo(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.resource    = attrs.get("resource")
+        self.action      = attrs.get("action")
+        self.permissions = attrs.get("permissions")
+
+    def type_coerce_attrs(self):
+        super(RangerAccessInfo, self).type_coerce_attrs()
+        self.resource = type_coerce(self.resource, RangerResourceInfo)
+
+
+class RangerAccessContext(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.serviceType          = attrs.get("serviceType")
+        self.serviceName          = attrs.get("serviceName")
+        self.accessTime           = attrs.get("accessTime")
+        self.clientIpAddress      = attrs.get("clientIpAddress")
+        self.forwardedIpAddresses = attrs.get("forwardedIpAddresses")
+        self.additionalInfo       = attrs.get("additionalInfo")
+
+
+class RangerAuthzRequest(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.requestId = attrs.get("requestId")
+        self.user      = attrs.get("user")
+        self.access    = attrs.get("access")
+        self.context   = attrs.get("context")
+
+    def type_coerce_attrs(self):
+        super(RangerAuthzRequest, self).type_coerce_attrs()
+        self.user    = type_coerce(self.user, RangerUserInfo)
+        self.access  = type_coerce(self.access, RangerAccessInfo)
+        self.context = type_coerce(self.context, RangerAccessContext)
+
+
+class RangerMultiAuthzRequest(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+
+        self.requestId = attrs.get("requestId")
+        self.user      = attrs.get("user")
+        self.accesses  = attrs.get("accesses")
+        self.context   = attrs.get("context")
+
+    def type_coerce_attrs(self):
+        super(RangerMultiAuthzRequest, self).type_coerce_attrs()
+        self.user     = type_coerce(self.user, RangerUserInfo)
+        self.accesses = type_coerce_list(self.accesses, RangerAccessInfo)
+        self.context  = type_coerce(self.context, RangerAccessContext)
+
+
+class RangerPolicyInfo(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.id      = attrs.get("id")
+        self.version = attrs.get("version")
+
+
+class RangerAccessResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.decision = attrs.get("decision")
+        self.policy   = attrs.get("policy")
+
+    def type_coerce_attrs(self):
+        super(RangerAccessResult, self).type_coerce_attrs()
+        self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerDataMaskResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.maskType    = attrs.get("maskType")
+        self.maskedValue = attrs.get("maskedValue")
+        self.policy      = attrs.get("policy")
+
+    def type_coerce_attrs(self):
+        super(RangerDataMaskResult, self).type_coerce_attrs()
+        self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerRowFilterResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.filterExpr = attrs.get("filterExpr")
+        self.policy     = attrs.get("policy")
+
+    def type_coerce_attrs(self):
+        super(RangerRowFilterResult, self).type_coerce_attrs()
+        self.policy = type_coerce(self.policy, RangerPolicyInfo)
+
+
+class RangerResultInfo(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.access         = attrs.get("access")
+        self.dataMask       = attrs.get("dataMask")
+        self.rowFilter      = attrs.get("rowFilter")
+        self.additionalInfo = attrs.get("additionalInfo")
+
+    def type_coerce_attrs(self):
+        super(RangerResultInfo, self).type_coerce_attrs()
+        self.access    = type_coerce(self.access, RangerAccessResult)
+        self.dataMask  = type_coerce(self.dataMask, RangerDataMaskResult)
+        self.rowFilter = type_coerce(self.rowFilter, RangerRowFilterResult)
+
+
+class RangerPermissionResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.permission     = attrs.get("permission")
+        self.access         = attrs.get("access")
+        self.dataMask       = attrs.get("dataMask")
+        self.rowFilter      = attrs.get("rowFilter")
+        self.additionalInfo = attrs.get("additionalInfo")
+        self.subResources   = attrs.get("subResources")
+
+    def type_coerce_attrs(self):
+        super(RangerPermissionResult, self).type_coerce_attrs()
+        self.access       = type_coerce(self.access, RangerAccessResult)
+        self.dataMask     = type_coerce(self.dataMask, RangerDataMaskResult)
+        self.rowFilter    = type_coerce(self.rowFilter, RangerRowFilterResult)
+        self.subResources = type_coerce_dict(self.subResources, 
RangerResultInfo)
+
+
+class RangerAuthzResult(RangerBase):
+    DECISION_ALLOW          = "ALLOW"
+    DECISION_DENY           = "DENY"
+    DECISION_NOT_DETERMINED = "NOT_DETERMINED"
+    DECISION_PARTIAL        = "PARTIAL"
+
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.requestId   = attrs.get("requestId")
+        self.decision    = attrs.get("decision")
+        self.permissions = attrs.get("permissions")
+
+    def type_coerce_attrs(self):
+        super(RangerAuthzResult, self).type_coerce_attrs()
+        self.permissions = type_coerce_dict(self.permissions, 
RangerPermissionResult)
+
+
+class RangerMultiAuthzResult(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.requestId = attrs.get("requestId")
+        self.decision  = attrs.get("decision")
+        self.accesses  = attrs.get("accesses")
+
+    def type_coerce_attrs(self):
+        super(RangerMultiAuthzResult, self).type_coerce_attrs()
+        self.accesses = type_coerce_list(self.accesses, RangerAuthzResult)
+
+
+class RangerResourcePermissionsRequest(RangerBase):
+    def __init__(self, attrs=None):
+        attrs = non_null(attrs, {})
+        RangerBase.__init__(self, attrs)
+        self.resource = attrs.get("resource")
+        self.context  = attrs.get("context")
+
+    def type_coerce_attrs(self):
+        super(RangerResourcePermissionsRequest, self).type_coerce_attrs()

Review Comment:
   `RangerResourcePermissionsRequest` in the Java model includes a `requestId`, 
and the README example sets one, but the Python 
`RangerResourcePermissionsRequest` model doesn't define/serialize `requestId`. 
This makes it impossible for Python callers to send request IDs for 
correlation. Consider adding a `requestId` field (and type coercion if needed) 
to match the API model.
   ```suggestion
           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()
           # Ensure requestId, if provided, is represented as a string for 
correlation
           if self.requestId is not None:
               self.requestId = str(self.requestId)
   ```



##########
pdp/src/main/java/org/apache/ranger/pdp/config/RangerPdpConfig.java:
##########
@@ -0,0 +1,296 @@
+/*
+ * 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.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 = 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 getAuthTypes() {
+        return get(RangerPdpConstants.PROP_AUTH_TYPES, "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 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 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, 30);
+    }
+
+    public String getKerberosCookieDomain() {
+        return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_DOMAIN, 
"");
+    }
+
+    public String getKerberosCookiePath() {
+        return get(RangerPdpConstants.PROP_AUTHN_KERBEROS_KRB_COOKIE_PATH, 
"/");
+    }
+
+    public String getKerberosNameRules() {
+        return get(RangerPdpConstants.PROP_KRB_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 = 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) {
+        XMLUtils.loadConfig(in, props);
+
+        LOG.info("Loaded {} properties from {}", props.size(), source);
+    }

Review Comment:
   The log message in `parseHadoopXml()` reports `props.size()`, which is the 
cumulative size after merging properties from potentially multiple sources. 
This makes it look like each file loaded N properties even when it didn't. 
Consider tracking and logging the delta (size before/after) or logging 
`nList.getLength()`/properties count from the parsed document instead.



##########
pdp/src/main/java/org/apache/ranger/pdp/rest/RangerPdpREST.java:
##########
@@ -0,0 +1,498 @@
+/*
+ * 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 javax.ws.rs.core.Response.Status.BAD_REQUEST;
+import static javax.ws.rs.core.Response.Status.FORBIDDEN;
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
+import static javax.ws.rs.core.Response.Status.UNAUTHORIZED;
+import static 
org.apache.ranger.pdp.config.RangerPdpConstants.PROP_PDP_SERVICE_PREFIX;
+import static 
org.apache.ranger.pdp.config.RangerPdpConstants.PROP_SUFFIX_DELEGATION_USERS;
+import static 
org.apache.ranger.pdp.config.RangerPdpConstants.WILDCARD_SERVICE_NAME;
+
+/**
+ * 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();
+                }
+            }
+
+            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();
+                }
+            }
+
+            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();
+                }
+            }
+
+            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 {
+            boolean needsDelegation = isDelegationNeeded(caller, user) || 
isDelegationNeeded(access);
+
+            if (needsDelegation) {
+                if (!isDelegationUserForService(serviceName, caller)) {
+                    LOG.info("{} is not a delegation user in service {}", 
caller, serviceName);
+
+                    ret = Response.status(FORBIDDEN)
+                            .entity(new ErrorResponse(FORBIDDEN, caller + " is 
not authorized"))
+                            .build();
+                } else {
+                    ret = RESPONSE_OK;
+                }
+            } else {
+                ret = RESPONSE_OK;
+            }
+        }
+
+        return ret;
+    }
+
+    private Response validateCaller(String caller, RangerUserInfo user, 
List<RangerAccessInfo> accesses, String serviceName) {
+        final Response ret;
+
+        if (StringUtils.isBlank(caller)) {
+            ret = Response.status(UNAUTHORIZED)
+                    .entity(new ErrorResponse(UNAUTHORIZED, "Authentication 
required"))
+                    .build();
+        } else {
+            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 != 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());
+        }

Review Comment:
   `isDelegationNeeded(String caller, RangerUserInfo user)` treats a 
missing/blank `user` as requiring delegation (because `caller.equals(null)` is 
always false). This causes invalid requests that should be rejected as `400` by 
`RangerAuthorizer.validateUserInfo()` to instead be rejected as `403` unless 
the caller is configured as a delegation user. Consider returning `false` (or 
skipping delegation checks) when `user == null` or `user.getName()` is blank, 
so request validation errors surface as `BAD_REQUEST` instead of `FORBIDDEN`.



##########
pdp/src/main/java/org/apache/ranger/pdp/security/KerberosAuthHandler.java:
##########
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.pdp.security;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.security.authentication.util.KerberosName;
+import org.apache.ranger.pdp.config.RangerPdpConstants;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.Oid;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.security.PrivilegedExceptionAction;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Authenticates requests using Kerberos SPNEGO (HTTP Negotiate).
+ *
+ * <p>Uses the JDK's built-in GSSAPI/JGSS support – no external Kerberos 
library is required.
+ * The service principal and keytab must be configured via:
+ * <ul>
+ *   <li>{@code ranger.pdp.kerberos.spnego.principal} – e.g. {@code 
HTTP/host.example.com@REALM}
+ *   <li>{@code ranger.pdp.kerberos.spnego.keytab}    – absolute path to the 
keytab file
+ *   <li>{@code hadoop.security.auth_to_local} – Hadoop-style name rules 
(default: {@code DEFAULT})
+ * </ul>
+ *
+ * <p>Authentication flow:
+ * <ol>
+ *   <li>If no {@code Authorization: Negotiate} header is present the handler 
returns {@code SKIP}.
+ *   <li>The SPNEGO token is extracted, validated via GSSAPI, and – if a 
response token is
+ *       produced (mutual authentication) – written to {@code 
WWW-Authenticate: Negotiate <token>}.
+ *   <li>On success the short-form principal name (strip {@literal @REALM} and 
host components)
+ *       is returned as the authenticated user.
+ *   <li>On failure a {@code 401 Negotiate} challenge is sent and {@code 
CHALLENGE} is returned.
+ * </ol>
+ */
+public class KerberosAuthHandler implements PdpAuthHandler {
+    private static final Logger LOG = 
LoggerFactory.getLogger(KerberosAuthHandler.class);
+
+    public static final String AUTH_TYPE = "KERBEROS";
+
+    private static final String NEGOTIATE_PREFIX    = "Negotiate ";
+    private static final String WWW_AUTHENTICATE    = "WWW-Authenticate";
+    private static final String AUTHORIZATION       = "Authorization";
+    private static final Oid    SPNEGO_OID;
+    private static final Oid    KRB5_OID;
+
+    static {
+        try {
+            SPNEGO_OID = new Oid("1.3.6.1.5.5.2");
+            KRB5_OID   = new Oid("1.2.840.113554.1.2.2");
+        } catch (GSSException e) {
+            throw new ExceptionInInitializerError(e);
+        }
+    }
+
+    private Subject serviceSubject;
+
+    @Override
+    public void init(Properties config) throws Exception {
+        String principal = 
config.getProperty(RangerPdpAuthFilter.PARAM_SPNEGO_PRINCIPAL);
+        String keytab    = 
config.getProperty(RangerPdpAuthFilter.PARAM_SPNEGO_KEYTAB);
+
+        if (StringUtils.isBlank(principal) || StringUtils.isBlank(keytab)) {
+            throw new IllegalArgumentException("Kerberos auth requires 
configurations " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_PRINCIPAL + " 
and " + RangerPdpConstants.PROP_AUTHN_KERBEROS_SPNEGO_KEYTAB);
+        }
+
+        String configuredNameRules = 
config.getProperty(RangerPdpAuthFilter.PARAM_KRB_NAME_RULES, "DEFAULT");
+
+        serviceSubject = loginWithKeytab(principal, keytab);
+
+        initializeKerberosNameRules(configuredNameRules);
+
+        LOG.info("KerberosAuthHandler initialized; principal={}", principal);
+    }
+
+    @Override
+    public Result authenticate(HttpServletRequest request, HttpServletResponse 
response) throws IOException {
+        String authHeader = request.getHeader(AUTHORIZATION);
+
+        if (authHeader == null || !authHeader.startsWith(NEGOTIATE_PREFIX)) {
+            return Result.skip();
+        }
+
+        byte[] inputToken = 
Base64.decodeBase64(authHeader.substring(NEGOTIATE_PREFIX.length()).trim());
+
+        try {
+            return Subject.doAs(serviceSubject, 
(PrivilegedExceptionAction<Result>) () -> validateSpnegoToken(inputToken, 
response));
+        } catch (Exception e) {
+            LOG.warn("authenticate(): SPNEGO validation error", e);
+
+            sendUnauthorized(response, null);
+
+            return Result.challenge();
+        }
+    }
+
+    @Override
+    public String getChallengeHeader() {
+        return NEGOTIATE_PREFIX.trim();
+    }
+
+    private Result validateSpnegoToken(byte[] inputToken, HttpServletResponse 
response) throws GSSException, IOException {
+        GSSManager    manager    = GSSManager.getInstance();
+        GSSCredential serverCred = manager.createCredential(null, 
GSSCredential.INDEFINITE_LIFETIME, new Oid[] {SPNEGO_OID, KRB5_OID}, 
GSSCredential.ACCEPT_ONLY);
+        GSSContext    gssCtx     = manager.createContext(serverCred);
+

Review Comment:
   `validateSpnegoToken()` creates a new `GSSCredential` (and `GSSContext`) on 
every request. Creating server credentials can be relatively expensive; 
consider caching the acceptor `GSSCredential` (created after keytab login) and 
reusing it across requests, while still creating a fresh `GSSContext` per 
request.



-- 
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