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