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

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


The following commit(s) were added to refs/heads/ranger-2.10 by this push:
     new 4c8bbcc93 RANGER-5812: Extend service config validation to 
lookupResource and saved-service check (#1266) (#1319)
4c8bbcc93 is described below

commit 4c8bbcc9387c4ad77d4cdee2b0f5e35e431121e5
Author: PradeeP AgrawaL <[email protected]>
AuthorDate: Wed Oct 21 10:56:30 2026 +0530

    RANGER-5812: Extend service config validation to lookupResource and 
saved-service check (#1266) (#1319)
    
    - 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);

Reply via email to