You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by pr...@apache.org on 2026/10/16 18:40:29 UTC
(ranger) branch RANGER-5812_ranger210 created (now df33116cc)
This is an automated email from the ASF dual-hosted git repository.
pradeepagrawal8184 pushed a change to branch RANGER-5812_ranger210
in repository https://gitbox.apache.org/repos/asf/ranger.git
at df33116cc RANGER-5812: Extend service config validation to lookupResource and saved-service check (#1266)
This branch includes the following new commits:
new df33116cc RANGER-5812: Extend service config validation to lookupResource and saved-service check (#1266)
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(ranger) 01/01: RANGER-5812: Extend service config validation to lookupResource and saved-service check (#1266)
Posted by pr...@apache.org.
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 <pr...@apache.org>
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://127.0.0.1@external.example.com: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://evil.example@knox.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 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);