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());
