This is an automated email from the ASF dual-hosted git repository. pradeepagrawal8184 pushed a commit to branch RANGER-5812_ranger210 in repository https://gitbox.apache.org/repos/asf/ranger.git
commit df33116cc0b1f7f5b8afbdc0d922e15b09313ae0 Author: PradeeP AgrawaL <[email protected]> AuthorDate: Fri Oct 16 18:52:32 2026 +0530 RANGER-5812: Extend service config validation to lookupResource and saved-service check (#1266) - 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). --- security-admin/pom.xml | 14 +- .../java/org/apache/ranger/biz/ServiceMgr.java | 329 +++++++- .../java/org/apache/ranger/rest/ServiceREST.java | 9 + .../conf.dist/ranger-admin-default-site.xml | 6 + .../java/org/apache/ranger/biz/TestServiceMgr.java | 879 +++++++++++++++++++++ .../org/apache/ranger/rest/TestServiceREST.java | 46 ++ 6 files changed, 1260 insertions(+), 23 deletions(-) diff --git a/security-admin/pom.xml b/security-admin/pom.xml index 51924d6a4..081df7858 100644 --- a/security-admin/pom.xml +++ b/security-admin/pom.xml @@ -813,6 +813,18 @@ <version>${junit.jupiter.version}</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>${junit.jupiter.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-params</artifactId> + <version>${junit.jupiter.version}</version> + <scope>test</scope> + </dependency> <dependency> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> @@ -850,7 +862,7 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> - <version>2.18.1</version> + <version>${maven.surefire.plugin.version}</version> <configuration> <skipTests>${skipTests}</skipTests> <additionalClasspathElements> 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 f406bfceb..6202d0539 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 @@ -31,7 +31,9 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; @@ -80,6 +82,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 @@ -112,6 +116,10 @@ public class ServiceMgr { public List<String> lookupResource(String serviceName, ResourceLookupContext context, ServiceStore svcStore) throws Exception { List<String> ret = null; RangerService service = svcDBStore.getServiceByName(serviceName); + + if (service == null) { + throw new Exception("Service not found: " + serviceName); + } String authType = PropertiesUtil.getProperty(AUTHENTICATION_TYPE); String lookupPrincipal = SecureClientLogin.getPrincipal(PropertiesUtil.getProperty(LOOKUP_PRINCIPAL), PropertiesUtil.getProperty(HOST_NAME)); @@ -139,6 +147,8 @@ public List<String> lookupResource(String serviceName, ResourceLookupContext con Map<String, String> newConfigs = rangerSvcService.getConfigsWithDecryptedPassword(service); service.setConfigs(newConfigs); + + validateOutboundServiceConfigs(newConfigs); RangerBaseService svc = getRangerServiceByService(service, svcStore); @@ -198,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) { @@ -254,6 +244,301 @@ public VXResponse validateConfig(RangerService service, ServiceStore svcStore) t return ret; } + 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 configKey = entry.getKey(); + String requestValue = entry.getValue(); + if (requestValue == null || requestValue.trim().isEmpty()) { + continue; + } + + String savedValue = savedConfigs.get(configKey); + boolean savedIsEndpoint = savedValue != null && isConnectionEndpoint(savedValue); + boolean requestIsEndpoint = isConnectionEndpoint(requestValue); + + if (!savedIsEndpoint && !requestIsEndpoint) { + continue; + } + + if (savedValue == null) { + throw new Exception("Invalid value for configuration " + configKey + ": endpoint property is not present in saved service configuration"); + } + + if (!savedIsEndpoint) { + throw new Exception("Invalid value for configuration " + configKey + ": endpoint must match the saved service configuration"); + } + + ConnectionEndpointPin savedPin = parseConnectionEndpointPin(configKey, savedValue); + ConnectionEndpointPin requestPin = parseConnectionEndpointPin(configKey, requestValue); + if (!requestPin.matches(savedPin)) { + throw new Exception("Invalid value for configuration " + configKey + ": endpoint must match the saved service configuration"); + } + } + } + + private static boolean isConnectionEndpoint(String value) { + if (value == null) { + return false; + } + + String trimmed = value.trim(); + return looksLikeConnectionUrl(trimmed) || looksLikeJdbcAtHost(trimmed) || looksLikeHostPortEndpoint(trimmed); + } + + private static boolean looksLikeConnectionUrl(String value) { + return value != null && value.contains("://"); + } + + private static boolean looksLikeJdbcAtHost(String value) { + if (StringUtils.isBlank(value) || looksLikeConnectionUrl(value)) { + return false; + } + + return value.startsWith("jdbc:") && value.contains("@"); + } + + private static boolean looksLikeHostPortEndpoint(String value) { + if (StringUtils.isBlank(value) || looksLikeConnectionUrl(value) || value.contains("@")) { + return false; + } + + if (value.contains(",")) { + for (String segment : value.split(",")) { + if (!looksLikeHostPortSegment(segment.trim())) { + return false; + } + } + + return true; + } + + return looksLikeHostPortSegment(value.trim()); + } + + private static boolean looksLikeHostPortSegment(String segment) { + if (StringUtils.isBlank(segment)) { + return false; + } + + if (segment.startsWith("[")) { + int closeBracket = segment.indexOf(']'); + if (closeBracket <= 0) { + return false; + } + + if (closeBracket + 1 >= segment.length() || segment.charAt(closeBracket + 1) != ':') { + return false; + } + + return StringUtils.isNumeric(segment.substring(closeBracket + 2)); + } + + int colon = segment.lastIndexOf(':'); + if (colon <= 0 || colon >= segment.length() - 1) { + return false; + } + + return StringUtils.isNumeric(segment.substring(colon + 1)); + } + + private static ConnectionEndpointPin parseConnectionEndpointPin(String configKey, String rawValue) throws Exception { + String trimmed = rawValue.trim(); + if (looksLikeConnectionUrl(trimmed)) { + return parseConnectionUrlPin(configKey, trimmed); + } + + if (looksLikeJdbcAtHost(trimmed)) { + int at = trimmed.lastIndexOf('@'); + String afterAt = trimmed.substring(at + 1); + if (afterAt.startsWith("//")) { + return parseConnectionUrlPin(configKey, "jdbc://" + afterAt.substring(2)); + } + + List<String> authorityHostPorts = parseCommaSeparatedHostPorts(afterAt); + if (authorityHostPorts.isEmpty()) { + throw new Exception("Invalid value for configuration " + configKey + ": connection URL cannot be parsed securely for endpoint pinning"); + } + + return new ConnectionEndpointPin("jdbc-at", null, authorityHostPorts, ""); + } + + if (looksLikeHostPortEndpoint(trimmed)) { + List<String> authorityHostPorts = parseCommaSeparatedHostPorts(trimmed); + if (authorityHostPorts.isEmpty()) { + throw new Exception("Invalid value for configuration " + configKey + ": connection URL cannot be parsed securely for endpoint pinning"); + } + + return new ConnectionEndpointPin("", null, authorityHostPorts, ""); + } + + throw new Exception("Invalid value for configuration " + configKey + ": connection URL cannot be parsed securely for endpoint pinning"); + } + + private static ConnectionEndpointPin parseConnectionUrlPin(String configKey, String rawValue) throws Exception { + String trimmed = rawValue.trim(); + int schemeSep = trimmed.indexOf("://"); + if (schemeSep < 0) { + throw new Exception("Invalid value for configuration " + configKey + ": connection URL cannot be parsed securely for endpoint pinning"); + } + + String scheme = trimmed.substring(0, schemeSep).toLowerCase(Locale.ROOT); + String remainder = trimmed.substring(schemeSep + 3); + + String path = ""; + String authority = remainder; + int pathStart = remainder.indexOf('/'); + if (pathStart >= 0) { + authority = remainder.substring(0, pathStart); + path = normalizePathForPinning(remainder.substring(pathStart)); + } + + String userInfo = null; + String hostPortPart = authority; + if (authority.contains("@")) { + int at = authority.lastIndexOf('@'); + userInfo = authority.substring(0, at); + hostPortPart = authority.substring(at + 1); + } + + List<String> authorityHostPorts = parseCommaSeparatedHostPorts(hostPortPart); + if (authorityHostPorts.isEmpty() && StringUtils.isBlank(hostPortPart)) { + if (StringUtils.isBlank(path) || "/".equals(path)) { + throw new Exception("Invalid value for configuration " + configKey + ": connection URL cannot be parsed securely for endpoint pinning"); + } + } else if (authorityHostPorts.isEmpty()) { + throw new Exception("Invalid value for configuration " + configKey + ": connection URL cannot be parsed securely for endpoint pinning"); + } + + return new ConnectionEndpointPin(scheme, userInfo, authorityHostPorts, path); + } + + private static String normalizePathForPinning(String path) { + if (path == null) { + return ""; + } + int cut = path.length(); + for (int i = 0; i < path.length(); i++) { + char c = path.charAt(i); + if (c == '?' || c == '#') { + cut = i; + break; + } + } + return path.substring(0, cut); + } + + private static List<String> parseCommaSeparatedHostPorts(String hostPortPart) { + List<String> ret = new ArrayList<>(); + if (StringUtils.isBlank(hostPortPart)) { + return ret; + } + + for (String segment : hostPortPart.split(",")) { + String normalized = normalizeHostPortSegment(segment.trim()); + if (StringUtils.isNotBlank(normalized)) { + ret.add(normalized); + } + } + + return ret; + } + + private static String normalizeHostPortSegment(String segment) { + if (StringUtils.isBlank(segment)) { + return segment; + } + + if (segment.startsWith("[")) { + int closeBracket = segment.indexOf(']'); + if (closeBracket > 0) { + String host = segment.substring(1, closeBracket).toLowerCase(Locale.ROOT); + String portSuffix = closeBracket + 1 < segment.length() ? segment.substring(closeBracket + 1) : ""; + return host + portSuffix; + } + } + + int colonCount = StringUtils.countMatches(segment, ":"); + if (colonCount == 1) { + int colon = segment.indexOf(':'); + return segment.substring(0, colon).toLowerCase(Locale.ROOT) + segment.substring(colon); + } + + return segment.toLowerCase(Locale.ROOT); + } + + private static final class ConnectionEndpointPin { + private final String scheme; + private final String userInfo; + private final List<String> authorityHostPorts; + private final String path; + + private ConnectionEndpointPin(String scheme, String userInfo, List<String> authorityHostPorts, String path) { + this.scheme = scheme; + this.userInfo = StringUtils.isBlank(userInfo) ? null : userInfo; + this.authorityHostPorts = authorityHostPorts == null ? Collections.emptyList() : authorityHostPorts; + this.path = path == null ? "" : path; + } + + private boolean matches(ConnectionEndpointPin saved) { + return Objects.equals(scheme, saved.scheme) + && Objects.equals(userInfo, saved.userInfo) + && Objects.equals(path, saved.path) + && authorityHostPorts.equals(saved.authorityHostPorts); + } + } + + 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 c6c53c502..a76da647e 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 @@ -1201,6 +1201,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 ce9ac191d..61c1d64c5 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 new file mode 100644 index 000000000..e9669ffba --- /dev/null +++ b/security-admin/src/test/java/org/apache/ranger/biz/TestServiceMgr.java @@ -0,0 +1,879 @@ +/* + * 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.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; +import org.apache.ranger.plugin.model.RangerSecurityZone; +import org.apache.ranger.plugin.model.RangerService; +import org.apache.ranger.plugin.model.RangerServiceDef; +import org.apache.ranger.plugin.service.RangerBaseService; +import org.apache.ranger.plugin.service.RangerDefaultService; +import org.apache.ranger.plugin.service.ResourceLookupContext; +import org.apache.ranger.plugin.store.ServiceStore; +import org.apache.ranger.plugin.util.RangerRoles; +import org.apache.ranger.service.RangerServiceService; +import org.apache.ranger.view.VXResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @generated by Cursor + * @description <Unit Test for TestServiceMgr class> + */ +@ExtendWith(MockitoExtension.class) +@TestMethodOrder(MethodOrderer.MethodName.class) +public class TestServiceMgr { + private static void setField(Object target, Class<?> declaring, String fieldName, Object value) throws Exception { + Field f = declaring.getDeclaredField(fieldName); + f.setAccessible(true); + f.set(target, value); + } + + private static void wireValidateConfigSuccessPipeline(ServiceMgr mgr, ServiceStore store, String serviceType) throws Exception { + TimedExecutor exec = mock(TimedExecutor.class); + setField(mgr, ServiceMgr.class, "timedExecutor", exec); + + RangerServiceDef def = new RangerServiceDef(); + def.setName(serviceType); + def.setImplClass(RangerDefaultService.class.getName()); + when(store.getServiceDefByName(serviceType)).thenReturn(def); + + Map<String, Object> resp = new HashMap<>(); + resp.put("connectivityStatus", true); + when(exec.timedTask(any(ServiceMgr.ValidateCallable.class), anyLong(), any())).thenReturn(resp); + } + + @Test + public void test01_parseLong_andTimeoutFallbacks() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + Assertions.assertNull(mgr.parseLong("abc")); + Assertions.assertEquals(42L, mgr.parseLong("42")); + + RangerBaseService svc = mock(RangerBaseService.class); + when(svc.getConfigs()).thenReturn(Collections.emptyMap()); + when(svc.getServiceName()).thenReturn("s"); + when(svc.getServiceType()).thenReturn("hdfs"); + long v1 = mgr.getTimeoutValueInMilliSeconds("resource.lookup", svc, 1000L); + long v2 = mgr.getTimeoutValueInMilliSeconds("validate.config", svc, 10000L); + Assertions.assertEquals(1000L, v1); + Assertions.assertEquals(10000L, v2); + } + + @Test + public void test02_getRangerServiceByService_andByName() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + ServiceStore store = mock(ServiceStore.class); + RangerService svc = new RangerService(); + svc.setType("hdfs"); + svc.setName("s"); + RangerServiceDef def = new RangerServiceDef(); + def.setName("hdfs"); + def.setImplClass(RangerDefaultService.class.getName()); + when(store.getServiceDefByName("hdfs")).thenReturn(def); + RangerBaseService built = mgr.getRangerServiceByService(svc, store); + Assertions.assertNotNull(built); + Assertions.assertEquals("s", built.getServiceName()); + + when(store.getServiceByName("n1")).thenReturn(svc); + Assertions.assertNotNull(mgr.getRangerServiceByName("n1", store)); + } + + @Test + public void test03_validateConfig_populatesKerberosConfigsAndHandlesErrors() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + RangerServiceService svcService = mock(RangerServiceService.class); + TimedExecutor exec = mock(TimedExecutor.class); + setField(mgr, ServiceMgr.class, "rangerSvcService", svcService); + setField(mgr, ServiceMgr.class, "timedExecutor", exec); + + RangerService svc = new RangerService(); + svc.setName("s"); + svc.setType("hdfs"); + svc.setConfigs(new HashMap<>()); + ServiceStore store = mock(ServiceStore.class); + RangerServiceDef def = new RangerServiceDef(); + def.setName("hdfs"); + def.setImplClass(RangerDefaultService.class.getName()); + when(store.getServiceDefByName("hdfs")).thenReturn(def); + when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(new HashMap<>()); + + Map<String, Object> resp = new HashMap<>(); + resp.put("connectivityStatus", true); + when(exec.timedTask(any(ServiceMgr.ValidateCallable.class), any(Long.class), any())).thenReturn(resp); + VXResponse ok = mgr.validateConfig(svc, store); + Assertions.assertEquals(VXResponse.STATUS_SUCCESS, ok.getStatusCode()); + } + + @Test + public void test04_lookupResource_delegatesToService() 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); + TimedExecutor exec = mock(TimedExecutor.class); + setField(mgr, ServiceMgr.class, "timedExecutor", exec); + + RangerService svc = new RangerService(); + svc.setName("s"); + svc.setType("hdfs"); + svc.setConfigs(new HashMap<>()); + when(db.getServiceByName("s")).thenReturn(svc); + when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(new HashMap<>()); + ServiceStore store = mock(ServiceStore.class); + RangerServiceDef def = new RangerServiceDef(); + def.setName("hdfs"); + def.setImplClass(RangerDefaultService.class.getName()); + when(store.getServiceDefByName("hdfs")).thenReturn(def); + RangerBaseService built = mgr.getRangerServiceByService(svc, store); + ResourceLookupContext ctx = new ResourceLookupContext(); + List<String> expected = Arrays.asList("a", "b"); + // Use timed path + when(exec.timedTask(any(ServiceMgr.LookupCallable.class), any(Long.class), any())).thenReturn(expected); + List<String> out = mgr.lookupResource("s", ctx, store); + Assertions.assertEquals(expected, out); + } + + @Test + public void test05_isZoneAdmin_and_isZoneAuditor() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + SecurityZoneDBStore zstore = mock(SecurityZoneDBStore.class); + RangerBizUtil biz = mock(RangerBizUtil.class); + XXGroupUserDao groupUserDao = mock(XXGroupUserDao.class); + setField(mgr, ServiceMgr.class, "zoneStore", zstore); + setField(mgr, ServiceMgr.class, "rangerBizUtil", biz); + setField(mgr, ServiceMgr.class, "groupUserDao", groupUserDao); + + RangerSecurityZone z = new RangerSecurityZone(); + z.setName("z1"); + z.setAdminUsers(Arrays.asList("u1")); + z.setAdminUserGroups(Arrays.asList("g1")); + z.setAdminRoles(Arrays.asList("r1")); + z.setAuditUsers(Arrays.asList("u2")); + z.setAuditUserGroups(Arrays.asList("g2")); + z.setAuditRoles(Arrays.asList("r2")); + when(zstore.getSecurityZoneByName("z1")).thenReturn(z); + when(biz.getCurrentUserLoginId()).thenReturn("u1"); + // Admin by user + Assertions.assertTrue(mgr.isZoneAdmin("z1")); + // Auditor by user + when(biz.getCurrentUserLoginId()).thenReturn("u2"); + Assertions.assertTrue(mgr.isZoneAuditor("z1")); + } + + @Test + public void test07_getFilesInDirectory_existingAndMissing() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + Method m = ServiceMgr.class.getDeclaredMethod("getFilesInDirectory", String.class, List.class); + m.setAccessible(true); + List<java.net.URL> files = new ArrayList<>(); + // Directory may not exist; method should handle and leave list empty + m.invoke(mgr, "ranger-plugins/unknown", files); + Assertions.assertTrue(files.isEmpty()); + } + + @Test + public void test08_isUserOrUserGroupsInRole_userAndGroup() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + RoleDBStore rolesStore = mock(RoleDBStore.class); + setField(mgr, ServiceMgr.class, "rolesStore", rolesStore); + + RangerRole roleUser = new RangerRole(); + roleUser.setName("r1"); + roleUser.setUsers(Arrays.asList(new RangerRole.RoleMember("u1", false))); + RangerRole roleGroup = new RangerRole(); + roleGroup.setName("r2"); + roleGroup.setGroups(Arrays.asList(new RangerRole.RoleMember("g1", false))); + + RangerRoles rr = new RangerRoles(); + Set<RangerRole> set = new HashSet<>(); + set.add(roleUser); + set.add(roleGroup); + rr.setRangerRoles(set); + when(rolesStore.getRoles("", -1L)).thenReturn(rr); + + Method m = ServiceMgr.class.getDeclaredMethod("isUserOrUserGroupsInRole", String.class, Set.class, List.class); + m.setAccessible(true); + + // user in r1 + Assertions.assertTrue((Boolean) m.invoke(mgr, "u1", Collections.emptySet(), Arrays.asList("r1"))); + // group in r2 + Assertions.assertTrue((Boolean) m.invoke(mgr, "uX", new HashSet<>(Arrays.asList("g1")), Arrays.asList("r2"))); + // no match + Assertions.assertFalse((Boolean) m.invoke(mgr, "uX", Collections.emptySet(), Arrays.asList("r3"))); + } + + @Test + public void test09_validateConfig_rejectsLocalhostUrl() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + RangerServiceService svcService = mock(RangerServiceService.class); + TimedExecutor exec = mock(TimedExecutor.class); + setField(mgr, ServiceMgr.class, "rangerSvcService", svcService); + setField(mgr, ServiceMgr.class, "timedExecutor", exec); + + RangerService svc = new RangerService(); + svc.setName("s"); + svc.setType("hdfs"); + Map<String, String> cfg = new HashMap<>(); + cfg.put("namenode", "http://localhost:9870"); + svc.setConfigs(cfg); + ServiceStore store = mock(ServiceStore.class); + RangerServiceDef def = new RangerServiceDef(); + def.setName("hdfs"); + def.setImplClass(RangerDefaultService.class.getName()); + when(store.getServiceDefByName("hdfs")).thenReturn(def); + when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(cfg); + + try { + mgr.validateConfig(svc, store); + Assertions.fail("Expected exception for localhost URL"); + } catch (Exception expected) { + // ok + } + } + + @Test + public void test10_lookupResource_tagServiceDirectCall() 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); + + RangerService svc = new RangerService(); + svc.setName("s"); + svc.setType(org.apache.ranger.plugin.store.EmbeddedServiceDefsUtil.EMBEDDED_SERVICEDEF_TAG_NAME); + svc.setConfigs(new HashMap<>()); + when(db.getServiceByName("s")).thenReturn(svc); + when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(new HashMap<>()); + ServiceStore store = mock(ServiceStore.class); + RangerServiceDef def = new RangerServiceDef(); + def.setName(org.apache.ranger.plugin.store.EmbeddedServiceDefsUtil.EMBEDDED_SERVICEDEF_TAG_NAME); + def.setImplClass(RangerDefaultService.class.getName()); + when(store.getServiceDefByName(def.getName())).thenReturn(def); + + // RangerServiceTag should cause direct svc.lookupResource without timed executor + RangerBaseService built = mgr.getRangerServiceByService(svc, store); + ResourceLookupContext ctx = new ResourceLookupContext(); + // no NPE, may return null which is acceptable + mgr.lookupResource("s", ctx, store); + Assertions.assertTrue(true); + } + + @Test + public void test11_getRangerServiceByService_rejectsImplClassNotAssignableToRangerBaseService() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + ServiceStore store = mock(ServiceStore.class); + RangerService svc = new RangerService(); + svc.setType("evil"); + svc.setName("s"); + RangerServiceDef def = new RangerServiceDef(); + def.setName("evil"); + def.setImplClass(Thread.class.getName()); + when(store.getServiceDefByName("evil")).thenReturn(def); + + Assertions.assertThrows(ClassCastException.class, () -> mgr.getRangerServiceByService(svc, store)); + } + + @Test + public void test12_getRangerServiceByService_acceptsImplClassAssignableToRangerBaseService() throws Exception { + // Sanity check: a legitimate implClass that does extend RangerBaseService still works. + ServiceMgr mgr = new ServiceMgr(); + ServiceStore store = mock(ServiceStore.class); + RangerService svc = new RangerService(); + svc.setType("legit"); + svc.setName("s2"); + RangerServiceDef def = new RangerServiceDef(); + def.setName("legit"); + def.setImplClass(RangerDefaultService.class.getName()); + when(store.getServiceDefByName("legit")).thenReturn(def); + + RangerBaseService built = mgr.getRangerServiceByService(svc, store); + + Assertions.assertNotNull(built); + Assertions.assertEquals(RangerDefaultService.class, built.getClass()); + } + + static Stream<Arguments> ssrfBypassConfigValues() { + return Stream.of( + Arguments.of("jdbc.url", "jdbc:mysql://127.1:3306/db"), + Arguments.of("api.endpoint", "http://0x7f000001:8080"), + Arguments.of("redis.host", "2130706433:6379"), + Arguments.of("service.url", "http://::1:8080"), + Arguments.of("api.endpoint", "http://[email protected]:8080")); + } + + @ParameterizedTest + @MethodSource("ssrfBypassConfigValues") + public void test11_validateConfig_rejectsSSRFBypassAttempts(String configKey, String configValue) throws Exception { + ServiceMgr mgr = new ServiceMgr(); + RangerServiceService svcService = mock(RangerServiceService.class); + TimedExecutor exec = mock(TimedExecutor.class); + 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("hive"); + svc.setName("test-service"); + svc.setConfigs(bypassConfigs); + + ServiceStore store = mock(ServiceStore.class); + RangerServiceDef def = new RangerServiceDef(); + def.setName("hive"); + def.setImplClass(RangerDefaultService.class.getName()); + when(store.getServiceDefByName("hive")).thenReturn(def); + when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(bypassConfigs); + + Exception thrown = Assertions.assertThrows(Exception.class, () -> mgr.validateConfig(svc, store)); + Assertions.assertTrue( + thrown.getMessage().contains("is not allowed") || thrown.getMessage().contains("blocked host detected"), + "Unexpected message: " + thrown.getMessage()); + } + + @Test + public void test13_validateConfig_allowsLegitimateExternalUrls() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + RangerServiceService svcService = mock(RangerServiceService.class); + setField(mgr, ServiceMgr.class, "rangerSvcService", svcService); + + Map<String, String> legitimateConfigs = new HashMap<>(); + legitimateConfigs.put("jdbc.url", "jdbc:mysql://db.company.com:3306/prod"); + legitimateConfigs.put("api.endpoint", "https://api.github.com:443"); + legitimateConfigs.put("user.list", "hive,impala,trino"); // Non-URL config + + RangerService svc = new RangerService(); + svc.setType("hive"); + svc.setName("test-service"); + svc.setConfigs(legitimateConfigs); + + when(svcService.getConfigsWithDecryptedPassword(any(RangerService.class))).thenReturn(legitimateConfigs); + + VXResponse response = mgr.validateConfig(svc, null); + 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); + ServiceStore store = mock(ServiceStore.class); + setField(mgr, ServiceMgr.class, "svcDBStore", db); + setField(mgr, ServiceMgr.class, "rangerSvcService", svcService); + wireValidateConfigSuccessPipeline(mgr, store, "knox"); + + 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))) + .thenAnswer(invocation -> new HashMap<>(invocation.<RangerService>getArgument(0).getConfigs())); + + VXResponse response = mgr.validateConfig(request, store); + Assertions.assertNotNull(response); + Assertions.assertEquals(VXResponse.STATUS_SUCCESS, response.getStatusCode()); + } finally { + PropertiesUtil.getPropertiesMap().remove(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE); + } + } + + @Test + public void test18_validateConfig_requireSavedService_rejectsPortOrPathChange() 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"); + + RangerService saved = new RangerService(); + saved.setName("knox-prod"); + saved.setType("knox"); + saved.setConfigs(savedCfg); + + when(db.getServiceByName("knox-prod")).thenReturn(saved); + when(svcService.getConfigsWithDecryptedPassword(saved)).thenReturn(savedCfg); + + Map<String, String> wrongPort = new HashMap<>(); + wrongPort.put("knox.url", "https://knox.example.com:9/gateway"); + RangerService requestPort = new RangerService(); + requestPort.setName("knox-prod"); + requestPort.setType("knox"); + requestPort.setConfigs(wrongPort); + when(svcService.getConfigsWithDecryptedPassword(requestPort)).thenReturn(wrongPort); + + Exception portEx = Assertions.assertThrows(Exception.class, () -> mgr.validateConfig(requestPort, null)); + Assertions.assertTrue(portEx.getMessage().contains("must match the saved service configuration")); + + Map<String, String> wrongPath = new HashMap<>(); + wrongPath.put("knox.url", "https://knox.example.com:8443/other"); + RangerService requestPath = new RangerService(); + requestPath.setName("knox-prod"); + requestPath.setType("knox"); + requestPath.setConfigs(wrongPath); + when(svcService.getConfigsWithDecryptedPassword(requestPath)).thenReturn(wrongPath); + + Exception pathEx = Assertions.assertThrows(Exception.class, () -> mgr.validateConfig(requestPath, null)); + Assertions.assertTrue(pathEx.getMessage().contains("must match the saved service configuration")); + } finally { + PropertiesUtil.getPropertiesMap().remove(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE); + } + } + + @Test + public void test19_validateConfig_requireSavedService_rejectsUserInfoBypass() 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://[email protected]: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 test20_validateConfig_requireSavedService_rejectsJdbcMultiHostMismatch() 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("jdbc.url", "jdbc:mysql://knox.example.com:3306/db"); + + Map<String, String> requestCfg = new HashMap<>(); + requestCfg.put("jdbc.url", "jdbc:mysql://knox.example.com:3306,evil.example.com:3306/db"); + + RangerService saved = new RangerService(); + saved.setName("hive-prod"); + saved.setType("hive"); + saved.setConfigs(savedCfg); + + RangerService request = new RangerService(); + request.setName("hive-prod"); + request.setType("hive"); + request.setConfigs(requestCfg); + + when(db.getServiceByName("hive-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 test21_validateConfig_requireSavedService_allowsUsernameChange() throws Exception { + ServiceMgr mgr = new ServiceMgr(); + ServiceDBStore db = mock(ServiceDBStore.class); + RangerServiceService svcService = mock(RangerServiceService.class); + ServiceStore store = mock(ServiceStore.class); + setField(mgr, ServiceMgr.class, "svcDBStore", db); + setField(mgr, ServiceMgr.class, "rangerSvcService", svcService); + wireValidateConfigSuccessPipeline(mgr, store, "knox"); + + 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"); + savedCfg.put("username", "admin"); + + Map<String, String> requestCfg = new HashMap<>(savedCfg); + requestCfg.put("username", "admin2"); + + 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(any(RangerService.class))) + .thenAnswer(invocation -> new HashMap<>(invocation.<RangerService>getArgument(0).getConfigs())); + + VXResponse response = mgr.validateConfig(request, store); + Assertions.assertNotNull(response); + Assertions.assertEquals(VXResponse.STATUS_SUCCESS, response.getStatusCode()); + } finally { + PropertiesUtil.getPropertiesMap().remove(ServiceMgr.PROP_SERVICE_CONFIG_VALIDATE_REQUIRE_SAVED_SERVICE); + } + } + + @Test + public void test22_validateConfig_requireSavedService_rejectsOpaqueJdbcUrlChange() 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("jdbc.url", "jdbc:mysql:loadbalance://knox.example.com:3306/db"); + + Map<String, String> requestCfg = new HashMap<>(); + requestCfg.put("jdbc.url", "jdbc:mysql:loadbalance://evil.example.com:3306/db"); + + RangerService saved = new RangerService(); + saved.setName("hive-prod"); + saved.setType("hive"); + saved.setConfigs(savedCfg); + + RangerService request = new RangerService(); + request.setName("hive-prod"); + request.setType("hive"); + request.setConfigs(requestCfg); + + when(db.getServiceByName("hive-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 test23_validateConfig_requireSavedService_rejectsHive2PathWhenHostNotParsed() 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("jdbc.url", "jdbc:hive2:///default"); + + Map<String, String> requestCfg = new HashMap<>(); + requestCfg.put("jdbc.url", "jdbc:hive2:///attacker"); + + RangerService saved = new RangerService(); + saved.setName("hive-prod"); + saved.setType("hive"); + saved.setConfigs(savedCfg); + + RangerService request = new RangerService(); + request.setName("hive-prod"); + request.setType("hive"); + request.setConfigs(requestCfg); + + when(db.getServiceByName("hive-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 test24_validateConfig_requireSavedService_rejectsHostPortWithoutScheme() 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", "evil.example.com:8443"); + + 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 test25_validateConfig_requireSavedService_rejectsOracleThinHostChange() 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("jdbc.url", "jdbc:oracle:thin:@knox.example.com:1521:ORCL"); + + Map<String, String> requestCfg = new HashMap<>(); + requestCfg.put("jdbc.url", "jdbc:oracle:thin:@evil.example.com:1521:ORCL"); + + RangerService saved = new RangerService(); + saved.setName("oracle-prod"); + saved.setType("oracle"); + saved.setConfigs(savedCfg); + + RangerService request = new RangerService(); + request.setName("oracle-prod"); + request.setType("oracle"); + request.setConfigs(requestCfg); + + when(db.getServiceByName("oracle-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 test26_validateConfig_requireSavedService_rejectsZkQuorumChange() 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("zookeeper.connect", "zk1.example.com:2181,zk2.example.com:2181"); + + Map<String, String> requestCfg = new HashMap<>(); + requestCfg.put("zookeeper.connect", "zk1.example.com:2181,evil.example.com:2181"); + + RangerService saved = new RangerService(); + saved.setName("kafka-prod"); + saved.setType("kafka"); + saved.setConfigs(savedCfg); + + RangerService request = new RangerService(); + request.setName("kafka-prod"); + request.setType("kafka"); + request.setConfigs(requestCfg); + + when(db.getServiceByName("kafka-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); + } + } +} 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 026d10c57..fc4043071 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 @@ -793,10 +793,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); Assert.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 = Assert.assertThrows(WebApplicationException.class, + () -> serviceREST.lookupResource(serviceName, context)); + + Assert.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 = Assert.assertThrows(WebApplicationException.class, + () -> serviceREST.lookupResource(serviceName, context)); + + Assert.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);
