This is an automated email from the ASF dual-hosted git repository.

pradeepagrawal8184 pushed a commit to branch RANGER-5812_master1
in repository https://gitbox.apache.org/repos/asf/ranger.git

commit 88f7a0e62e8c4d5b1f18241b93a374f55a751abc
Author: Pradeep Agrawal <[email protected]>
AuthorDate: Tue Sep 29 17:29:27 2026 +0530

    RANGER-5812: Extend service config validation to lookupResource and 
saved-service check
    
    - Refactor validateConfig SSRF checks into validateOutboundServiceConfigs 
and reuse
      for lookupResource before Admin initiates outbound resource lookups.
    - Enforce service-level hasAccess in ServiceREST.lookupResource.
    - Add optional ranger.serviceconfig.validate.require.saved.service 
validateConfig
      hardening (default false).
---
 .../java/org/apache/ranger/biz/ServiceMgr.java     | 116 +++++++++++++-----
 .../java/org/apache/ranger/rest/ServiceREST.java   |   9 ++
 .../conf.dist/ranger-admin-default-site.xml        |   6 +
 .../java/org/apache/ranger/biz/TestServiceMgr.java | 135 +++++++++++++++++++++
 .../org/apache/ranger/rest/TestServiceREST.java    |  51 ++++++++
 5 files changed, 289 insertions(+), 28 deletions(-)

diff --git a/security-admin/src/main/java/org/apache/ranger/biz/ServiceMgr.java 
b/security-admin/src/main/java/org/apache/ranger/biz/ServiceMgr.java
index 515dd98d9..cbd18b6c3 100755
--- a/security-admin/src/main/java/org/apache/ranger/biz/ServiceMgr.java
+++ b/security-admin/src/main/java/org/apache/ranger/biz/ServiceMgr.java
@@ -79,6 +79,8 @@ public class ServiceMgr {
     private static final String NAME_RULES           = 
"hadoop.security.auth_to_local";
     private static final String HOST_NAME            = "ranger.service.host";
 
+    public static final String 
PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE = 
"ranger.serviceconfig.validate.require.saved.service";
+
     private static final long _DefaultTimeoutValue_Lookp          = 1000; // 1 
s
     private static final long _DefaultTimeoutValue_ValidateConfig = 10000; // 
10 s
 
@@ -137,15 +139,17 @@ public List<String> lookupResource(String serviceName, 
ResourceLookupContext con
             }
         }
 
-        RangerBaseService svc = null;
+        if (service == null) {
+            throw new Exception("Service not found: " + serviceName);
+        }
 
-        if (service != null) {
-            Map<String, String> newConfigs = 
rangerSvcService.getConfigsWithDecryptedPassword(service);
+        Map<String, String> newConfigs = 
rangerSvcService.getConfigsWithDecryptedPassword(service);
 
-            service.setConfigs(newConfigs);
+        service.setConfigs(newConfigs);
 
-            svc = getRangerServiceByService(service, svcStore);
-        }
+        validateOutboundServiceConfigs(newConfigs);
+
+        RangerBaseService svc = getRangerServiceByService(service, svcStore);
 
         LOG.debug("==> ServiceMgr.lookupResource for Service: ({}Context: 
{})", svc, context);
 
@@ -204,29 +208,9 @@ public VXResponse validateConfig(RangerService service, 
ServiceStore svcStore) t
 
         LOG.debug("==> ServiceMgr.validateConfig for Service: ({})", svc);
 
-        // check if service configs contains localhost/127.0.0.1
         if (service != null && service.getConfigs() != null) {
-            for (Map.Entry<String, String> entry : 
service.getConfigs().entrySet()) {
-                String configValue = entry.getValue();
-                if (configValue != null && !configValue.trim().isEmpty()) {
-                    String userInfo = extractUserInfo(configValue);
-                    if (userInfoContainsBlockedHost(userInfo)) {
-                        throw new Exception("Invalid value for configuration " 
+ entry.getKey() + ": userinfo contains blocked host (blocked host detected)");
-                    }
-
-                    String host = extractHost(configValue);
-                    if (host != null) {
-                        if (isBlockedHost(host)) {
-                            throw new Exception("Invalid value for 
configuration " + entry.getKey() + ": host " + host + " is not allowed (blocked 
host detected)");
-                        }
-                    } else {
-                        String lowerVal = configValue.toLowerCase().trim();
-                        if (lowerVal.contains("localhost") || 
lowerVal.contains("127.0.0.1") || lowerVal.contains("0.0.0.0") || 
lowerVal.contains("::1")) {
-                            throw new Exception("Invalid value for 
configuration " + entry.getKey() + ": contains blocked keywords but could not 
be parsed securely.");
-                        }
-                    }
-                }
-            }
+            enforceSavedServiceEndpointConfigs(service);
+            validateOutboundServiceConfigs(service.getConfigs());
         }
 
         if (svc != null) {
@@ -460,6 +444,82 @@ Long parseLong(String str) {
         }
     }
 
+    private void enforceSavedServiceEndpointConfigs(RangerService service) 
throws Exception {
+        if 
(!Boolean.parseBoolean(PropertiesUtil.getProperty(PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE,
 "false"))) {
+            return;
+        }
+
+        String serviceName = service.getName();
+        if (StringUtils.isBlank(serviceName)) {
+            return;
+        }
+
+        RangerService savedService = svcDBStore.getServiceByName(serviceName);
+        if (savedService == null) {
+            throw new Exception("Configuration validation requires a saved 
service: " + serviceName);
+        }
+
+        if (StringUtils.isNotBlank(service.getType()) && 
StringUtils.isNotBlank(savedService.getType())
+                && !StringUtils.equals(service.getType(), 
savedService.getType())) {
+            throw new Exception("Configuration validation service type does 
not match saved service: " + serviceName);
+        }
+
+        Map<String, String> savedConfigs = 
rangerSvcService.getConfigsWithDecryptedPassword(savedService);
+        if (savedConfigs == null) {
+            savedConfigs = Collections.emptyMap();
+        }
+
+        for (Map.Entry<String, String> entry : 
service.getConfigs().entrySet()) {
+            String requestValue = entry.getValue();
+            if (requestValue == null || requestValue.trim().isEmpty()) {
+                continue;
+            }
+
+            String requestHost = extractHost(requestValue);
+            if (requestHost == null) {
+                continue;
+            }
+
+            String savedValue = savedConfigs.get(entry.getKey());
+            if (savedValue == null) {
+                throw new Exception("Invalid value for configuration " + 
entry.getKey() + ": endpoint property is not present in saved service 
configuration");
+            }
+
+            String savedHost = extractHost(savedValue);
+            if (savedHost == null || !requestHost.equalsIgnoreCase(savedHost)) 
{
+                throw new Exception("Invalid value for configuration " + 
entry.getKey() + ": endpoint must match the saved service configuration");
+            }
+        }
+    }
+
+    private static void validateOutboundServiceConfigs(Map<String, String> 
configs) throws Exception {
+        if (configs == null) {
+            return;
+        }
+
+        for (Map.Entry<String, String> entry : configs.entrySet()) {
+            String configValue = entry.getValue();
+            if (configValue != null && !configValue.trim().isEmpty()) {
+                String userInfo = extractUserInfo(configValue);
+                if (userInfoContainsBlockedHost(userInfo)) {
+                    throw new Exception("Invalid value for configuration " + 
entry.getKey() + ": userinfo contains blocked host (blocked host detected)");
+                }
+
+                String host = extractHost(configValue);
+                if (host != null) {
+                    if (isBlockedHost(host)) {
+                        throw new Exception("Invalid value for configuration " 
+ entry.getKey() + ": host " + host + " is not allowed (blocked host 
detected)");
+                    }
+                } else {
+                    String lowerVal = configValue.toLowerCase().trim();
+                    if (lowerVal.contains("localhost") || 
lowerVal.contains("127.0.0.1") || lowerVal.contains("0.0.0.0") || 
lowerVal.contains("::1")) {
+                        throw new Exception("Invalid value for configuration " 
+ entry.getKey() + ": contains blocked keywords but could not be parsed 
securely.");
+                    }
+                }
+            }
+        }
+    }
+
     private static URI toConfigUri(String urlString) throws Exception {
         String processedUrl = urlString.trim().replaceFirst("(?i)^jdbc:", "");
         if (!processedUrl.contains("://")) {
diff --git 
a/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java 
b/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java
index 25534c214..84f6e3604 100644
--- a/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java
+++ b/security-admin/src/main/java/org/apache/ranger/rest/ServiceREST.java
@@ -1135,6 +1135,15 @@ public List<String> 
lookupResource(@PathParam("serviceName") String serviceName,
         RangerPerfTracer perf = null;
 
         try {
+            XXService xxService = 
daoManager.getXXService().findByName(serviceName);
+            if (xxService == null) {
+                throw 
restErrorUtil.createRESTException(HttpServletResponse.SC_NOT_FOUND, "Not 
found", true);
+            }
+            if (!Boolean.TRUE.equals(bizUtil.hasAccess(xxService, null))) {
+                throw 
restErrorUtil.createRESTException(HttpServletResponse.SC_FORBIDDEN,
+                        "Operation denied. User is not permitted to lookup 
resources for service " + serviceName, true);
+            }
+
             if (RangerPerfTracer.isPerfTraceEnabled(PERF_LOG)) {
                 perf = RangerPerfTracer.getPerfTracer(PERF_LOG, 
"ServiceREST.lookupResource(serviceName=" + serviceName + ")");
             }
diff --git 
a/security-admin/src/main/resources/conf.dist/ranger-admin-default-site.xml 
b/security-admin/src/main/resources/conf.dist/ranger-admin-default-site.xml
index aa71f7dff..4f7bc87ac 100644
--- a/security-admin/src/main/resources/conf.dist/ranger-admin-default-site.xml
+++ b/security-admin/src/main/resources/conf.dist/ranger-admin-default-site.xml
@@ -508,6 +508,12 @@
                <description></description>
        </property>
 
+       <property>
+               <name>ranger.serviceconfig.validate.require.saved.service</name>
+               <value>false</value>
+               <description>When true, validateConfig for a named service 
requires a saved service record and rejects endpoint URLs that differ from the 
saved configuration.</description>
+       </property>
+
        <property>
                <name>ranger.timed.executor.max.threadpool.size</name>
                <value>10</value>
diff --git 
a/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java 
b/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java
index ecf16c510..d26940dc6 100644
--- a/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java
+++ b/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java
@@ -17,6 +17,7 @@
 
 package org.apache.ranger.biz;
 
+import org.apache.ranger.common.PropertiesUtil;
 import org.apache.ranger.common.TimedExecutor;
 import org.apache.ranger.db.XXGroupUserDao;
 import org.apache.ranger.plugin.model.RangerRole;
@@ -385,4 +386,138 @@ public void 
test13_validateConfig_allowsLegitimateExternalUrls() throws Exceptio
         Assertions.assertNotNull(response);
         Assertions.assertNotEquals(VXResponse.STATUS_ERROR, 
response.getStatusCode());
     }
+
+    @ParameterizedTest
+    @MethodSource("ssrfBypassConfigValues")
+    public void test14_lookupResource_rejectsSSRFBypassAttempts(String 
configKey, String configValue) throws Exception {
+        ServiceMgr mgr = new ServiceMgr();
+        ServiceDBStore svcDBStore = mock(ServiceDBStore.class);
+        RangerServiceService svcService = mock(RangerServiceService.class);
+        TimedExecutor exec = mock(TimedExecutor.class);
+        setField(mgr, ServiceMgr.class, "svcDBStore", svcDBStore);
+        setField(mgr, ServiceMgr.class, "rangerSvcService", svcService);
+        setField(mgr, ServiceMgr.class, "timedExecutor", exec);
+
+        Map<String, String> bypassConfigs = new HashMap<>();
+        bypassConfigs.put(configKey, configValue);
+
+        RangerService svc = new RangerService();
+        svc.setType("knox");
+        svc.setName("knox-test");
+        svc.setConfigs(bypassConfigs);
+
+        when(svcDBStore.getServiceByName("knox-test")).thenReturn(svc);
+        
when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(bypassConfigs);
+
+        ResourceLookupContext ctx = new ResourceLookupContext();
+        Exception thrown = Assertions.assertThrows(Exception.class, () -> 
mgr.lookupResource("knox-test", ctx, null));
+        Assertions.assertTrue(
+                thrown.getMessage().contains("is not allowed") || 
thrown.getMessage().contains("blocked host detected"),
+                "Unexpected message: " + thrown.getMessage());
+    }
+
+    @Test
+    public void 
test15_validateConfig_requireSavedService_rejectsMissingService() throws 
Exception {
+        ServiceMgr mgr = new ServiceMgr();
+        ServiceDBStore db = mock(ServiceDBStore.class);
+        RangerServiceService svcService = mock(RangerServiceService.class);
+        setField(mgr, ServiceMgr.class, "svcDBStore", db);
+        setField(mgr, ServiceMgr.class, "rangerSvcService", svcService);
+
+        
PropertiesUtil.getPropertiesMap().put(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE,
 "true");
+        try {
+            Map<String, String> cfg = new HashMap<>();
+            cfg.put("knox.url", "https://knox.example.com:8443/gateway";);
+
+            RangerService svc = new RangerService();
+            svc.setName("knox-prod");
+            svc.setType("knox");
+            svc.setConfigs(cfg);
+
+            
when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(cfg);
+            when(db.getServiceByName("knox-prod")).thenReturn(null);
+
+            Exception thrown = Assertions.assertThrows(Exception.class, () -> 
mgr.validateConfig(svc, null));
+            Assertions.assertTrue(thrown.getMessage().contains("requires a 
saved service"));
+        } finally {
+            
PropertiesUtil.getPropertiesMap().remove(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE);
+        }
+    }
+
+    @Test
+    public void 
test16_validateConfig_requireSavedService_rejectsEndpointMismatch() throws 
Exception {
+        ServiceMgr mgr = new ServiceMgr();
+        ServiceDBStore db = mock(ServiceDBStore.class);
+        RangerServiceService svcService = mock(RangerServiceService.class);
+        setField(mgr, ServiceMgr.class, "svcDBStore", db);
+        setField(mgr, ServiceMgr.class, "rangerSvcService", svcService);
+
+        
PropertiesUtil.getPropertiesMap().put(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE,
 "true");
+        try {
+            Map<String, String> savedCfg = new HashMap<>();
+            savedCfg.put("knox.url", "https://knox.example.com:8443/gateway";);
+
+            Map<String, String> requestCfg = new HashMap<>();
+            requestCfg.put("knox.url", 
"https://attacker.example.com:8443/gateway";);
+
+            RangerService saved = new RangerService();
+            saved.setName("knox-prod");
+            saved.setType("knox");
+            saved.setConfigs(savedCfg);
+
+            RangerService request = new RangerService();
+            request.setName("knox-prod");
+            request.setType("knox");
+            request.setConfigs(requestCfg);
+
+            when(db.getServiceByName("knox-prod")).thenReturn(saved);
+            
when(svcService.getConfigsWithDecryptedPassword(saved)).thenReturn(savedCfg);
+            
when(svcService.getConfigsWithDecryptedPassword(request)).thenReturn(requestCfg);
+
+            Exception thrown = Assertions.assertThrows(Exception.class, () -> 
mgr.validateConfig(request, null));
+            Assertions.assertTrue(thrown.getMessage().contains("must match the 
saved service configuration"));
+        } finally {
+            
PropertiesUtil.getPropertiesMap().remove(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE);
+        }
+    }
+
+    @Test
+    public void 
test17_validateConfig_requireSavedService_allowsMatchingEndpoint() throws 
Exception {
+        ServiceMgr mgr = new ServiceMgr();
+        ServiceDBStore db = mock(ServiceDBStore.class);
+        RangerServiceService svcService = mock(RangerServiceService.class);
+        setField(mgr, ServiceMgr.class, "svcDBStore", db);
+        setField(mgr, ServiceMgr.class, "rangerSvcService", svcService);
+
+        
PropertiesUtil.getPropertiesMap().put(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE,
 "true");
+        try {
+            Map<String, String> cfg = new HashMap<>();
+            cfg.put("knox.url", "https://knox.example.com:8443/gateway";);
+            cfg.put("username", "admin");
+
+            RangerService saved = new RangerService();
+            saved.setName("knox-prod");
+            saved.setType("knox");
+            saved.setConfigs(cfg);
+
+            RangerService request = new RangerService();
+            request.setName("knox-prod");
+            request.setType("knox");
+            request.setConfigs(new HashMap<>(cfg));
+
+            when(db.getServiceByName("knox-prod")).thenReturn(saved);
+            
when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(cfg);
+
+            try {
+                mgr.validateConfig(request, null);
+            } catch (Exception e) {
+                Assertions.assertFalse(e.getMessage().contains("must match the 
saved service configuration"),
+                        "Saved-service endpoint check should allow matching 
hosts: " + e.getMessage());
+                Assertions.assertFalse(e.getMessage().contains("requires a 
saved service"),
+                        "Saved-service endpoint check should not require 
missing service: " + e.getMessage());
+            }
+        } finally {
+            
PropertiesUtil.getPropertiesMap().remove(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE);
+        }
+    }
 }
diff --git 
a/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java 
b/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java
index b08a4f52d..bc208f314 100644
--- a/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java
+++ b/security-admin/src/test/java/org/apache/ranger/rest/TestServiceREST.java
@@ -633,10 +633,56 @@ public void test13lookupResource() throws Exception {
         ResourceLookupContext context     = new ResourceLookupContext();
         context.setResourceName(serviceName);
         context.setUserInput("HDFS");
+        XXService    xService    = xService();
+        XXServiceDao xServiceDao = Mockito.mock(XXServiceDao.class);
+        Mockito.when(daoManager.getXXService()).thenReturn(xServiceDao);
+        Mockito.when(xServiceDao.findByName(serviceName)).thenReturn(xService);
+        Mockito.when(bizUtil.hasAccess(xService, null)).thenReturn(true);
         List<String> list = serviceREST.lookupResource(serviceName, context);
         Assertions.assertNotNull(list);
     }
 
+    @Test
+    public void test13bLookupResourceNotFound() throws Exception {
+        String                serviceName = "missing_service";
+        ResourceLookupContext context     = new ResourceLookupContext();
+        XXServiceDao          xServiceDao = Mockito.mock(XXServiceDao.class);
+
+        Mockito.when(daoManager.getXXService()).thenReturn(xServiceDao);
+        Mockito.when(xServiceDao.findByName(serviceName)).thenReturn(null);
+        
Mockito.when(restErrorUtil.createRESTException(Mockito.eq(HttpServletResponse.SC_NOT_FOUND),
 Mockito.eq("Not found"), Mockito.eq(true)))
+                .thenReturn(new 
WebApplicationException(HttpServletResponse.SC_NOT_FOUND));
+
+        WebApplicationException exception = 
Assertions.assertThrows(WebApplicationException.class,
+                () -> serviceREST.lookupResource(serviceName, context));
+
+        Assertions.assertEquals(HttpServletResponse.SC_NOT_FOUND, 
exception.getResponse().getStatus());
+        
Mockito.verify(restErrorUtil).createRESTException(HttpServletResponse.SC_NOT_FOUND,
 "Not found", true);
+        Mockito.verify(serviceMgr, 
Mockito.never()).lookupResource(Mockito.anyString(), Mockito.any(), 
Mockito.any());
+    }
+
+    @Test
+    public void test13cLookupResourceForbiddenWhenNoAccess() throws Exception {
+        String                serviceName = "HDFS_1";
+        ResourceLookupContext context     = new ResourceLookupContext();
+        XXService             xService    = xService();
+        XXServiceDao          xServiceDao = Mockito.mock(XXServiceDao.class);
+        String                deniedMsg   = "Operation denied. User is not 
permitted to lookup resources for service " + serviceName;
+
+        Mockito.when(daoManager.getXXService()).thenReturn(xServiceDao);
+        Mockito.when(xServiceDao.findByName(serviceName)).thenReturn(xService);
+        Mockito.when(bizUtil.hasAccess(xService, null)).thenReturn(false);
+        
Mockito.when(restErrorUtil.createRESTException(Mockito.eq(HttpServletResponse.SC_FORBIDDEN),
 Mockito.eq(deniedMsg), Mockito.eq(true)))
+                .thenReturn(new 
WebApplicationException(HttpServletResponse.SC_FORBIDDEN));
+
+        WebApplicationException exception = 
Assertions.assertThrows(WebApplicationException.class,
+                () -> serviceREST.lookupResource(serviceName, context));
+
+        Assertions.assertEquals(HttpServletResponse.SC_FORBIDDEN, 
exception.getResponse().getStatus());
+        
Mockito.verify(restErrorUtil).createRESTException(HttpServletResponse.SC_FORBIDDEN,
 deniedMsg, true);
+        Mockito.verify(serviceMgr, 
Mockito.never()).lookupResource(Mockito.anyString(), Mockito.any(), 
Mockito.any());
+    }
+
     @Test
     public void test14grantAccess() throws Exception {
         HttpServletRequest request         = 
Mockito.mock(HttpServletRequest.class);
@@ -3302,6 +3348,11 @@ public void test116LookupResourceWithException() throws 
Exception {
         context.setResourceName(serviceName);
         context.setUserInput("HDFS");
 
+        XXService    xService    = xService();
+        XXServiceDao xServiceDao = Mockito.mock(XXServiceDao.class);
+        Mockito.when(daoManager.getXXService()).thenReturn(xServiceDao);
+        Mockito.when(xServiceDao.findByName(serviceName)).thenReturn(xService);
+        Mockito.when(bizUtil.hasAccess(xService, null)).thenReturn(true);
         Mockito.when(serviceMgr.lookupResource(serviceName, context, 
svcStore)).thenThrow(new RuntimeException("Lookup failed"));
         
Mockito.when(restErrorUtil.createRESTException(Mockito.anyString())).thenReturn(new
 WebApplicationException());
 

Reply via email to